From 72d32bcded6f41ae58ffe4104303caefd88008cb Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 3 Sep 2024 11:34:49 +0200 Subject: [PATCH 01/22] FiberRefs perf work (#3518) --- .changeset/twelve-dingos-destroy.md | 6 + packages/effect/src/Fiber.ts | 35 ++++++ packages/effect/src/internal/core.ts | 9 +- .../effect/src/internal/defaultServices.ts | 19 ++- packages/effect/src/internal/fiberRefs.ts | 3 +- packages/effect/src/internal/fiberRuntime.ts | 116 +++++++++--------- packages/effect/src/internal/layer.ts | 6 +- .../effect/src/internal/managedRuntime.ts | 2 +- packages/effect/src/internal/runtime.ts | 2 +- packages/opentelemetry/src/internal/tracer.ts | 5 +- 10 files changed, 119 insertions(+), 84 deletions(-) create mode 100644 .changeset/twelve-dingos-destroy.md diff --git a/.changeset/twelve-dingos-destroy.md b/.changeset/twelve-dingos-destroy.md new file mode 100644 index 0000000000..e59d3d3a07 --- /dev/null +++ b/.changeset/twelve-dingos-destroy.md @@ -0,0 +1,6 @@ +--- +"@effect/opentelemetry": minor +"effect": minor +--- + +Cache some fiber references in the runtime to optimize reading in hot-paths diff --git a/packages/effect/src/Fiber.ts b/packages/effect/src/Fiber.ts index 15f8623a3f..3f18046372 100644 --- a/packages/effect/src/Fiber.ts +++ b/packages/effect/src/Fiber.ts @@ -2,6 +2,8 @@ * @since 2.0.0 */ import type * as Cause from "./Cause.js" +import type { Context } from "./Context.js" +import type { DefaultServices } from "./DefaultServices.js" import type * as Effect from "./Effect.js" import type * as Either from "./Either.js" import type * as Exit from "./Exit.js" @@ -18,7 +20,10 @@ import type * as Option from "./Option.js" import type * as order from "./Order.js" import type { Pipeable } from "./Pipeable.js" import type * as RuntimeFlags from "./RuntimeFlags.js" +import type { Scheduler } from "./Scheduler.js" import type * as Scope from "./Scope.js" +import type { Supervisor } from "./Supervisor.js" +import type { AnySpan, Tracer } from "./Tracer.js" import type * as Types from "./Types.js" /** @@ -155,6 +160,36 @@ export interface RuntimeFiber extends Fiber, Fiber.R * resume immediately. Otherwise, the effect will resume when the fiber exits. */ unsafeInterruptAsFork(fiberId: FiberId.FiberId): void + + /** + * Gets the current context + */ + get currentContext(): Context + + /** + * Gets the current context + */ + get currentDefaultServices(): Context + + /** + * Gets the current scheduler + */ + get currentScheduler(): Scheduler + + /** + * Gets the current tracer + */ + get currentTracer(): Tracer + + /** + * Gets the current span + */ + get currentSpan(): AnySpan | undefined + + /** + * Gets the current supervisor + */ + get currentSupervisor(): Supervisor } /** diff --git a/packages/effect/src/internal/core.ts b/packages/effect/src/internal/core.ts index 5bc0e55a1d..fb4e941318 100644 --- a/packages/effect/src/internal/core.ts +++ b/packages/effect/src/internal/core.ts @@ -51,7 +51,6 @@ import * as DeferredOpCodes from "./opCodes/deferred.js" import * as OpCodes from "./opCodes/effect.js" import * as _runtimeFlags from "./runtimeFlags.js" import { SingleShotGen } from "./singleShotGen.js" -import * as internalTracer from "./tracer.js" // ----------------------------------------------------------------------------- // Effect @@ -1684,7 +1683,7 @@ const fiberRefVariance = { /* @internal */ export const fiberRefGet = (self: FiberRef.FiberRef): Effect.Effect => - fiberRefModify(self, (a) => [a, a] as const) + withFiberRuntime((fiber) => exitSucceed(fiber.getFiberRef(self))) /* @internal */ export const fiberRefGetAndSet = dual< @@ -2966,7 +2965,7 @@ const deferredInterruptJoiner = ( // Context // ----------------------------------------------------------------------------- -const constContext = fiberRefGet(currentContext) +const constContext = withFiberRuntime((fiber) => exitSucceed(fiber.currentContext)) /* @internal */ export const context = (): Effect.Effect, never, R> => constContext as any @@ -3021,9 +3020,7 @@ export const mapInputContext = dual< /** @internal */ export const currentSpanFromFiber = (fiber: Fiber.RuntimeFiber): Option.Option => { - const span = fiber.getFiberRef(currentContext).unsafeMap.get(internalTracer.spanTag.key) as - | Tracer.AnySpan - | undefined + const span = fiber.currentSpan return span !== undefined && span._tag === "Span" ? Option.some(span) : Option.none() } diff --git a/packages/effect/src/internal/defaultServices.ts b/packages/effect/src/internal/defaultServices.ts index 18f96fc4b5..6d63576608 100644 --- a/packages/effect/src/internal/defaultServices.ts +++ b/packages/effect/src/internal/defaultServices.ts @@ -47,9 +47,14 @@ export const sleep = (duration: Duration.DurationInput): Effect.Effect => return clockWith((clock) => clock.sleep(decodedDuration)) } +/** @internal */ +export const defaultServicesWith = ( + f: (services: Context.Context) => Effect.Effect +) => core.withFiberRuntime((fiber) => f(fiber.currentDefaultServices)) + /** @internal */ export const clockWith = (f: (clock: Clock.Clock) => Effect.Effect): Effect.Effect => - core.fiberRefGetWith(currentServices, (services) => f(Context.get(services, clock.clockTag))) + defaultServicesWith((services) => f(services.unsafeMap.get(clock.clockTag.key))) /** @internal */ export const currentTimeMillis: Effect.Effect = clockWith((clock) => clock.currentTimeMillis) @@ -83,10 +88,7 @@ export const withConfigProvider = dual< export const configProviderWith = ( f: (configProvider: ConfigProvider.ConfigProvider) => Effect.Effect ): Effect.Effect => - core.fiberRefGetWith( - currentServices, - (services) => f(Context.get(services, configProvider.configProviderTag)) - ) + defaultServicesWith((services) => f(services.unsafeMap.get(configProvider.configProviderTag.key))) /** @internal */ export const config = (config: Config.Config) => configProviderWith((_) => _.load(config)) @@ -98,10 +100,7 @@ export const configOrDie = (config: Config.Config) => core.orDie(configPro /** @internal */ export const randomWith = (f: (random: Random.Random) => Effect.Effect): Effect.Effect => - core.fiberRefGetWith( - currentServices, - (services) => f(Context.get(services, random.randomTag)) - ) + defaultServicesWith((services) => f(services.unsafeMap.get(random.randomTag.key))) /** @internal */ export const withRandom = dual< @@ -151,7 +150,7 @@ export const choice = >( /** @internal */ export const tracerWith = (f: (tracer: Tracer.Tracer) => Effect.Effect): Effect.Effect => - core.fiberRefGetWith(currentServices, (services) => f(Context.get(services, tracer.tracerTag))) + defaultServicesWith((services) => f(services.unsafeMap.get(tracer.tracerTag.key))) /** @internal */ export const withTracer = dual< diff --git a/packages/effect/src/internal/fiberRefs.ts b/packages/effect/src/internal/fiberRefs.ts index 3b3e26b32c..8d9ece313b 100644 --- a/packages/effect/src/internal/fiberRefs.ts +++ b/packages/effect/src/internal/fiberRefs.ts @@ -33,8 +33,7 @@ export class FiberRefsImpl implements FiberRefs.FiberRefs { FiberRef.FiberRef, Arr.NonEmptyReadonlyArray > - ) { - } + ) {} pipe() { return pipeArguments(this, arguments) } diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index 446394dff7..a4b99c3980 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -6,6 +6,7 @@ import * as Chunk from "../Chunk.js" import type * as Clock from "../Clock.js" import type { ConfigProvider } from "../ConfigProvider.js" import * as Context from "../Context.js" +import type { DefaultServices } from "../DefaultServices.js" import * as Deferred from "../Deferred.js" import type * as Duration from "../Duration.js" import type * as Effect from "../Effect.js" @@ -169,8 +170,8 @@ const contOpSuccess = { cont: core.RevertFlags, value: unknown ) => { - self.patchRuntimeFlags(self._runtimeFlags, cont.patch) - if (_runtimeFlags.interruptible(self._runtimeFlags) && self.isInterrupted()) { + self.patchRuntimeFlags(self.currentRuntimeFlags, cont.patch) + if (_runtimeFlags.interruptible(self.currentRuntimeFlags) && self.isInterrupted()) { return core.exitFailCause(self.getInterruptedCause()) } else { return core.exitSucceed(value) @@ -278,8 +279,6 @@ export class FiberRuntime implements Fiber.RuntimeFi private _fiberRefs: FiberRefs.FiberRefs private _fiberId: FiberId.Runtime - public _runtimeFlags: RuntimeFlags.RuntimeFlags - private _queue = new Array() private _children: Set> | null = null private _observers = new Array<(exit: Exit.Exit) => void>() @@ -289,28 +288,31 @@ export class FiberRuntime implements Fiber.RuntimeFi private _asyncBlockingOn: FiberId.FiberId | null = null private _exitValue: Exit.Exit | null = null private _steps: Array = [] - public _supervisor: Supervisor.Supervisor - public _scheduler: Scheduler - private _tracer: Tracer.Tracer + private _isYielding = false + + public currentRuntimeFlags: RuntimeFlags.RuntimeFlags public currentOpCount: number = 0 - private isYielding = false + public currentSupervisor!: Supervisor.Supervisor + public currentScheduler!: Scheduler + public currentTracer!: Tracer.Tracer + public currentSpan!: Tracer.AnySpan | undefined + public currentContext!: Context.Context + public currentDefaultServices!: Context.Context constructor( fiberId: FiberId.Runtime, fiberRefs0: FiberRefs.FiberRefs, runtimeFlags0: RuntimeFlags.RuntimeFlags ) { - this._runtimeFlags = runtimeFlags0 + this.currentRuntimeFlags = runtimeFlags0 this._fiberId = fiberId this._fiberRefs = fiberRefs0 - this._supervisor = this.getFiberRef(currentSupervisor) - this._scheduler = this.getFiberRef(currentScheduler) if (_runtimeFlags.runtimeMetrics(runtimeFlags0)) { const tags = this.getFiberRef(core.currentMetricLabels) fiberStarted.unsafeUpdate(1, tags) fiberActive.unsafeUpdate(1, tags) } - this._tracer = Context.get(this.getFiberRef(defaultServices.currentServices), tracer.tracerTag) + this.refreshRefCache() } /** @@ -342,7 +344,7 @@ export class FiberRuntime implements Fiber.RuntimeFi get runtimeFlags(): Effect.Effect { return this.ask((state, status) => { if (FiberStatus.isDone(status)) { - return state._runtimeFlags + return state.currentRuntimeFlags } return status.runtimeFlags }) @@ -528,7 +530,7 @@ export class FiberRuntime implements Fiber.RuntimeFi * log annotations and log level) may not be up-to-date. */ getFiberRefs(): FiberRefs.FiberRefs { - this.setFiberRef(currentRuntimeFlags, this._runtimeFlags) + this.setFiberRef(currentRuntimeFlags, this.currentRuntimeFlags) return this._fiberRefs } @@ -570,9 +572,12 @@ export class FiberRuntime implements Fiber.RuntimeFi } refreshRefCache() { - this._tracer = Context.get(this.getFiberRef(defaultServices.currentServices), tracer.tracerTag) - this._supervisor = this.getFiberRef(currentSupervisor) - this._scheduler = this.getFiberRef(currentScheduler) + this.currentDefaultServices = this.getFiberRef(defaultServices.currentServices) + this.currentTracer = this.currentDefaultServices.unsafeMap.get(tracer.tracerTag.key) + this.currentSupervisor = this.getFiberRef(currentSupervisor) + this.currentScheduler = this.getFiberRef(currentScheduler) + this.currentContext = this.getFiberRef(core.currentContext) + this.currentSpan = this.currentContext.unsafeMap.get(tracer.spanTag.key) } /** @@ -653,7 +658,7 @@ export class FiberRuntime implements Fiber.RuntimeFi * **NOTE**: This method must be invoked by the fiber itself. */ drainQueueLaterOnExecutor() { - this._scheduler.scheduleTask( + this.currentScheduler.scheduleTask( this.run, this.getFiberRef(core.currentSchedulingPriority) ) @@ -764,7 +769,7 @@ export class FiberRuntime implements Fiber.RuntimeFi } reportExitValue(exit: Exit.Exit) { - if (_runtimeFlags.runtimeMetrics(this._runtimeFlags)) { + if (_runtimeFlags.runtimeMetrics(this.currentRuntimeFlags)) { const tags = this.getFiberRef(core.currentMetricLabels) const startTimeMillis = this.id().startTimeMillis const endTimeMillis = Date.now() @@ -866,7 +871,7 @@ export class FiberRuntime implements Fiber.RuntimeFi this, this._exitValue !== null ? FiberStatus.done : - FiberStatus.suspended(this._runtimeFlags, this._asyncBlockingOn!) + FiberStatus.suspended(this.currentRuntimeFlags, this._asyncBlockingOn!) ) return EvaluationSignalContinue } @@ -882,10 +887,10 @@ export class FiberRuntime implements Fiber.RuntimeFi * **NOTE**: This method must be invoked by the fiber itself. */ evaluateEffect(effect0: Effect.Effect) { - this._supervisor.onResume(this) + this.currentSupervisor.onResume(this) try { let effect: Effect.Effect | null = - _runtimeFlags.interruptible(this._runtimeFlags) && this.isInterrupted() ? + _runtimeFlags.interruptible(this.currentRuntimeFlags) && this.isInterrupted() ? core.exitFailCause(this.getInterruptedCause()) : effect0 while (effect !== null) { @@ -895,7 +900,7 @@ export class FiberRuntime implements Fiber.RuntimeFi const op = yieldedOpChannel.currentOp! yieldedOpChannel.currentOp = null if (op._op === OpCodes.OP_YIELD) { - if (_runtimeFlags.cooperativeYielding(this._runtimeFlags)) { + if (_runtimeFlags.cooperativeYielding(this.currentRuntimeFlags)) { this.tell(FiberMessage.yieldNow()) this.tell(FiberMessage.resume(core.exitVoid)) effect = null @@ -907,7 +912,7 @@ export class FiberRuntime implements Fiber.RuntimeFi effect = null } } else { - this._runtimeFlags = pipe(this._runtimeFlags, _runtimeFlags.enable(_runtimeFlags.WindDown)) + this.currentRuntimeFlags = pipe(this.currentRuntimeFlags, _runtimeFlags.enable(_runtimeFlags.WindDown)) const interruption = this.interruptAllChildren() if (interruption !== null) { effect = core.flatMap(interruption, () => exit) @@ -926,7 +931,7 @@ export class FiberRuntime implements Fiber.RuntimeFi } } } finally { - this._supervisor.onSuspend(this) + this.currentSupervisor.onSuspend(this) } } @@ -981,7 +986,7 @@ export class FiberRuntime implements Fiber.RuntimeFi patchRuntimeFlags(oldRuntimeFlags: RuntimeFlags.RuntimeFlags, patch: RuntimeFlagsPatch.RuntimeFlagsPatch) { const newRuntimeFlags = _runtimeFlags.patch(oldRuntimeFlags, patch) ;(globalThis as any)[internalFiber.currentFiberURI] = this - this._runtimeFlags = newRuntimeFlags + this.currentRuntimeFlags = newRuntimeFlags return newRuntimeFlags } @@ -1016,7 +1021,7 @@ export class FiberRuntime implements Fiber.RuntimeFi pushStack(cont: core.Continuation) { this._stack.push(cont) if (cont._op === "OnStep") { - this._steps.push({ refs: this.getFiberRefs(), flags: this._runtimeFlags }) + this._steps.push({ refs: this.getFiberRefs(), flags: this.currentRuntimeFlags }) } } @@ -1052,10 +1057,7 @@ export class FiberRuntime implements Fiber.RuntimeFi } [OpCodes.OP_TAG](op: core.Primitive & { _op: OpCodes.OP_SYNC }) { - return core.map( - core.fiberRefGet(core.currentContext), - (context) => Context.unsafeGet(context, op as unknown as Context.Tag) - ) + return core.sync(() => Context.unsafeGet(this.currentContext, op as unknown as Context.Tag)) } ["Left"](op: core.Primitive & { _op: "Left" }) { @@ -1144,22 +1146,22 @@ export class FiberRuntime implements Fiber.RuntimeFi switch (cont._op) { case OpCodes.OP_ON_FAILURE: case OpCodes.OP_ON_SUCCESS_AND_FAILURE: { - if (!(_runtimeFlags.interruptible(this._runtimeFlags) && this.isInterrupted())) { + if (!(_runtimeFlags.interruptible(this.currentRuntimeFlags) && this.isInterrupted())) { return internalCall(() => cont.effect_instruction_i1(cause)) } else { return core.exitFailCause(internalCause.stripFailures(cause)) } } case "OnStep": { - if (!(_runtimeFlags.interruptible(this._runtimeFlags) && this.isInterrupted())) { + if (!(_runtimeFlags.interruptible(this.currentRuntimeFlags) && this.isInterrupted())) { return core.exitSucceed(core.exitFailCause(cause)) } else { return core.exitFailCause(internalCause.stripFailures(cause)) } } case OpCodes.OP_REVERT_FLAGS: { - this.patchRuntimeFlags(this._runtimeFlags, cont.patch) - if (_runtimeFlags.interruptible(this._runtimeFlags) && this.isInterrupted()) { + this.patchRuntimeFlags(this.currentRuntimeFlags, cont.patch) + if (_runtimeFlags.interruptible(this.currentRuntimeFlags) && this.isInterrupted()) { return core.exitFailCause(internalCause.sequential(cause, this.getInterruptedCause())) } else { return core.exitFailCause(cause) @@ -1179,14 +1181,14 @@ export class FiberRuntime implements Fiber.RuntimeFi return internalCall(() => op.effect_instruction_i0( this as FiberRuntime, - FiberStatus.running(this._runtimeFlags) as FiberStatus.Running + FiberStatus.running(this.currentRuntimeFlags) as FiberStatus.Running ) ) } ["Blocked"](op: core.Primitive & { _op: "Blocked" }) { const refs = this.getFiberRefs() - const flags = this._runtimeFlags + const flags = this.currentRuntimeFlags if (this._steps.length > 0) { const frames: Array = [] const snap = this._steps[this._steps.length - 1] @@ -1196,7 +1198,7 @@ export class FiberRuntime implements Fiber.RuntimeFi frame = this.popStack() } this.setFiberRefs(snap.refs) - this._runtimeFlags = snap.flags + this.currentRuntimeFlags = snap.flags const patchRefs = FiberRefsPatch.diff(snap.refs, refs) const patchFlags = _runtimeFlags.diff(snap.flags, flags) return core.exitSucceed(core.blocked( @@ -1208,7 +1210,7 @@ export class FiberRuntime implements Fiber.RuntimeFi newFiber.setFiberRefs( FiberRefsPatch.patch(newFiber.id(), newFiber.getFiberRefs())(patchRefs) ) - newFiber._runtimeFlags = _runtimeFlags.patch(patchFlags)(newFiber._runtimeFlags) + newFiber.currentRuntimeFlags = _runtimeFlags.patch(patchFlags)(newFiber.currentRuntimeFlags) return op.effect_instruction_i1 }) )) @@ -1227,7 +1229,7 @@ export class FiberRuntime implements Fiber.RuntimeFi [OpCodes.OP_UPDATE_RUNTIME_FLAGS](op: core.Primitive & { _op: OpCodes.OP_UPDATE_RUNTIME_FLAGS }) { const updateFlags = op.effect_instruction_i0 - const oldRuntimeFlags = this._runtimeFlags + const oldRuntimeFlags = this.currentRuntimeFlags const newRuntimeFlags = _runtimeFlags.patch(oldRuntimeFlags, updateFlags) // One more chance to short circuit: if we're immediately going // to interrupt. Interruption will cause immediate reversion of @@ -1237,7 +1239,7 @@ export class FiberRuntime implements Fiber.RuntimeFi return core.exitFailCause(this.getInterruptedCause()) } else { // Impossible to short circuit, so record the changes - this.patchRuntimeFlags(this._runtimeFlags, updateFlags) + this.patchRuntimeFlags(this.currentRuntimeFlags, updateFlags) if (op.effect_instruction_i1) { // Since we updated the flags, we need to revert them const revertFlags = _runtimeFlags.diff(newRuntimeFlags, oldRuntimeFlags) @@ -1271,13 +1273,13 @@ export class FiberRuntime implements Fiber.RuntimeFi [OpCodes.OP_ASYNC](op: core.Primitive & { _op: OpCodes.OP_ASYNC }) { this._asyncBlockingOn = op.effect_instruction_i1 - this.initiateAsync(this._runtimeFlags, op.effect_instruction_i0) + this.initiateAsync(this.currentRuntimeFlags, op.effect_instruction_i0) yieldedOpChannel.currentOp = op return YieldedOp } [OpCodes.OP_YIELD](op: core.Primitive & { op: OpCodes.OP_YIELD }) { - this.isYielding = false + this._isYielding = false yieldedOpChannel.currentOp = op return YieldedOp } @@ -1307,17 +1309,17 @@ export class FiberRuntime implements Fiber.RuntimeFi this.currentOpCount = 0 while (true) { - if ((this._runtimeFlags & OpSupervision) !== 0) { - this._supervisor.onEffect(this, cur) + if ((this.currentRuntimeFlags & OpSupervision) !== 0) { + this.currentSupervisor.onEffect(this, cur) } if (this._queue.length > 0) { - cur = this.drainQueueWhileRunning(this._runtimeFlags, cur) + cur = this.drainQueueWhileRunning(this.currentRuntimeFlags, cur) } - if (!this.isYielding) { + if (!this._isYielding) { this.currentOpCount += 1 - const shouldYield = this._scheduler.shouldYield(this) + const shouldYield = this.currentScheduler.shouldYield(this) if (shouldYield !== false) { - this.isYielding = true + this._isYielding = true this.currentOpCount = 0 const oldCur = cur cur = core.flatMap(core.yieldNow({ priority: shouldYield }), () => oldCur) @@ -1330,7 +1332,7 @@ export class FiberRuntime implements Fiber.RuntimeFi } // @ts-expect-error - cur = this._tracer.context( + cur = this.currentTracer.context( () => { if (version.getCurrentVersion() !== (cur as core.Primitive)[EffectTypeId]._V) { return core.dieMessage( @@ -1604,12 +1606,12 @@ export const addFinalizer = ( core.withFiberRuntime( (runtime) => { const acquireRefs = runtime.getFiberRefs() - const acquireFlags = runtime._runtimeFlags + const acquireFlags = runtime.currentRuntimeFlags return core.flatMap(scope, (scope) => core.scopeAddFinalizerExit(scope, (exit) => core.withFiberRuntime((runtimeFinalizer) => { const preRefs = runtimeFinalizer.getFiberRefs() - const preFlags = runtimeFinalizer._runtimeFlags + const preFlags = runtimeFinalizer.currentRuntimeFlags const patchRefs = FiberRefsPatch.diff(preRefs, acquireRefs) const patchFlags = _runtimeFlags.diff(preFlags, acquireFlags) const inverseRefs = FiberRefsPatch.diff(acquireRefs, preRefs) @@ -2100,7 +2102,7 @@ export const forEachConcurrentDiscard = ( const results = new Array() const interruptAll = () => fibers.forEach((fiber) => { - fiber._scheduler.scheduleTask(() => { + fiber.currentScheduler.scheduleTask(() => { fiber.unsafeInterruptAsFork(parent.id()) }, 0) }) @@ -2122,10 +2124,10 @@ export const forEachConcurrentDiscard = ( const fiber = unsafeForkUnstarted( runnable, parent, - parent._runtimeFlags, + parent.currentRuntimeFlags, fiberScope.globalScope ) - parent._scheduler.scheduleTask(() => { + parent.currentScheduler.scheduleTask(() => { if (interruptImmediately) { fiber.unsafeInterruptAsFork(parent.id()) } @@ -2186,7 +2188,7 @@ export const forEachConcurrentDiscard = ( startOrder.push(fiber) fibers.add(fiber) if (interrupted) { - fiber._scheduler.scheduleTask(() => { + fiber.currentScheduler.scheduleTask(() => { fiber.unsafeInterruptAsFork(parent.id()) }, 0) } @@ -2356,7 +2358,7 @@ export const unsafeMakeChildFiber = ( childFiberRefs, core.currentContext as unknown as FiberRef.FiberRef> ) - const supervisor = childFiber._supervisor + const supervisor = childFiber.currentSupervisor supervisor.onStart( childContext, diff --git a/packages/effect/src/internal/layer.ts b/packages/effect/src/internal/layer.ts index a1c61b6dba..ca60bd5414 100644 --- a/packages/effect/src/internal/layer.ts +++ b/packages/effect/src/internal/layer.ts @@ -1270,17 +1270,17 @@ const provideSomeRuntime = dual< const oldContext = fiber.getFiberRef(core.currentContext) const oldRefs = fiber.getFiberRefs() const newRefs = FiberRefsPatch.patch(fiber.id(), oldRefs)(patchRefs) - const oldFlags = fiber._runtimeFlags + const oldFlags = fiber.currentRuntimeFlags const newFlags = runtimeFlags.patch(patchFlags)(oldFlags) const rollbackRefs = FiberRefsPatch.diff(newRefs, oldRefs) const rollbackFlags = runtimeFlags.diff(newFlags, oldFlags) fiber.setFiberRefs(newRefs) - fiber._runtimeFlags = newFlags + fiber.currentRuntimeFlags = newFlags return fiberRuntime.ensuring( core.provideSomeContext(restore(self), Context.merge(oldContext, rt.context)), core.withFiberRuntime((fiber) => { fiber.setFiberRefs(FiberRefsPatch.patch(fiber.id(), fiber.getFiberRefs())(rollbackRefs)) - fiber._runtimeFlags = runtimeFlags.patch(rollbackFlags)(fiber._runtimeFlags) + fiber.currentRuntimeFlags = runtimeFlags.patch(rollbackFlags)(fiber.currentRuntimeFlags) return core.void }) ) diff --git a/packages/effect/src/internal/managedRuntime.ts b/packages/effect/src/internal/managedRuntime.ts index 5df50e6642..a647fabc57 100644 --- a/packages/effect/src/internal/managedRuntime.ts +++ b/packages/effect/src/internal/managedRuntime.ts @@ -26,7 +26,7 @@ function provide( (rt) => core.withFiberRuntime((fiber) => { fiber.setFiberRefs(rt.fiberRefs) - fiber._runtimeFlags = rt.runtimeFlags + fiber.currentRuntimeFlags = rt.runtimeFlags return core.provideContext(effect, rt.context) }) ) diff --git a/packages/effect/src/internal/runtime.ts b/packages/effect/src/internal/runtime.ts index bf93d5b7eb..2494b3f1a2 100644 --- a/packages/effect/src/internal/runtime.ts +++ b/packages/effect/src/internal/runtime.ts @@ -74,7 +74,7 @@ export const unsafeFork = (runtime: Runtime.Runtime) => ) } - const supervisor = fiberRuntime._supervisor + const supervisor = fiberRuntime.currentSupervisor // we can compare by reference here as _supervisor.none is wrapped with globalValue if (supervisor !== _supervisor.none) { diff --git a/packages/opentelemetry/src/internal/tracer.ts b/packages/opentelemetry/src/internal/tracer.ts index 2e0d13d340..5e0cf9c935 100644 --- a/packages/opentelemetry/src/internal/tracer.ts +++ b/packages/opentelemetry/src/internal/tracer.ts @@ -3,7 +3,6 @@ import * as Cause from "effect/Cause" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import type { Exit } from "effect/Exit" -import * as FiberRef from "effect/FiberRef" import * as Layer from "effect/Layer" import * as Option from "effect/Option" import * as EffectTracer from "effect/Tracer" @@ -143,9 +142,7 @@ export const make = Effect.map(Tracer, (tracer) => ) }, context(execution, fiber) { - const currentSpan = fiber.getFiberRef(FiberRef.currentContext).unsafeMap.get(EffectTracer.ParentSpan.key) as - | EffectTracer.AnySpan - | undefined + const currentSpan = fiber.currentSpan if (currentSpan === undefined) { return execution() From b4503ae322b15d5ec02bb1f9e8e8b33bc572e9d0 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 4 Sep 2024 11:32:18 +1200 Subject: [PATCH 02/22] refactor /platform HttpClient (#3537) --- .changeset/tender-foxes-walk.md | 87 ++++ .../cluster-node/examples/sample-connect.ts | 36 +- .../cluster-node/examples/sample-manager.ts | 31 +- .../cluster-node/examples/sample-shard.ts | 36 +- .../platform-browser/src/BrowserHttpClient.ts | 17 +- .../src/internal/httpClient.ts | 23 +- .../test/BrowserHttpClient.test.ts | 103 +++-- packages/platform-bun/examples/http-client.ts | 11 +- packages/platform-bun/src/BunHttpServer.ts | 2 +- .../platform-bun/src/internal/httpServer.ts | 9 +- packages/platform-node/examples/api.ts | 4 +- .../platform-node/examples/http-client.ts | 9 +- packages/platform-node/src/NodeHttpClient.ts | 32 +- packages/platform-node/src/NodeHttpServer.ts | 2 +- .../platform-node/src/internal/httpClient.ts | 6 +- .../src/internal/httpClientUndici.ts | 32 +- packages/platform-node/test/HttpApi.test.ts | 2 +- .../platform-node/test/HttpClient.test.ts | 57 ++- .../platform-node/test/HttpServer.test.ts | 378 ++++++++---------- packages/platform/README.md | 368 ++++++++--------- packages/platform/src/FetchHttpClient.ts | 25 ++ packages/platform/src/HttpApiClient.ts | 10 +- packages/platform/src/HttpClient.ts | 126 +++--- packages/platform/src/HttpClientRequest.ts | 42 +- packages/platform/src/HttpClientResponse.ts | 166 +------- packages/platform/src/HttpIncomingMessage.ts | 43 -- packages/platform/src/HttpServer.ts | 2 +- packages/platform/src/index.ts | 5 + .../platform/src/internal/fetchHttpClient.ts | 56 +++ packages/platform/src/internal/httpClient.ts | 228 ++++++----- .../src/internal/httpClientRequest.ts | 24 +- .../src/internal/httpClientResponse.ts | 102 +---- packages/platform/test/HttpClient.test.ts | 80 ++-- packages/rpc-http/examples/client.ts | 28 +- packages/rpc-http/src/HttpRpcResolver.ts | 29 +- .../rpc-http/src/HttpRpcResolverNoStream.ts | 27 +- 36 files changed, 1029 insertions(+), 1209 deletions(-) create mode 100644 .changeset/tender-foxes-walk.md create mode 100644 packages/platform/src/FetchHttpClient.ts create mode 100644 packages/platform/src/internal/fetchHttpClient.ts diff --git a/.changeset/tender-foxes-walk.md b/.changeset/tender-foxes-walk.md new file mode 100644 index 0000000000..d7574348dc --- /dev/null +++ b/.changeset/tender-foxes-walk.md @@ -0,0 +1,87 @@ +--- +"@effect/platform-browser": minor +"@effect/platform-node": minor +"@effect/platform-bun": minor +"@effect/platform": minor +"@effect/rpc-http": minor +--- + +refactor /platform HttpClient + +#### HttpClient.fetch removed + +The `HttpClient.fetch` client implementation has been removed. Instead, you can +access a `HttpClient` using the corresponding `Context.Tag`. + +```ts +import { FetchHttpClient, HttpClient } from "@effect/platform" +import { Effect } from "effect" + +Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + // make a get request + yield* client.get("https://jsonplaceholder.typicode.com/todos/1") +}).pipe( + Effect.scoped, + // the fetch client has been moved to the `FetchHttpClient` module + Effect.provide(FetchHttpClient.layer) +) +``` + +#### `HttpClient` interface now uses methods + +Instead of being a function that returns the response, the `HttpClient` +interface now uses methods to make requests. + +Some shorthand methods have been added to the `HttpClient` interface to make +less complex requests easier. + +```ts +import { + FetchHttpClient, + HttpClient, + HttpClientRequest +} from "@effect/platform" +import { Effect } from "effect" + +Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + // make a get request + yield* client.get("https://jsonplaceholder.typicode.com/todos/1") + // make a post request + yield* client.post("https://jsonplaceholder.typicode.com/todos") + + // execute a request instance + yield* client.execute( + HttpClientRequest.get("https://jsonplaceholder.typicode.com/todos/1") + ) +}) +``` + +#### Scoped `HttpClientResponse` helpers removed + +The `HttpClientResponse` helpers that also eliminated the `Scope` have been removed. + +Instead, you can use the `HttpClientResponse` methods directly, and explicitly +add a `Effect.scoped` to the pipeline. + +```ts +import { FetchHttpClient, HttpClient } from "@effect/platform" +import { Effect } from "effect" + +Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + yield* client.get("https://jsonplaceholder.typicode.com/todos/1").pipe( + Effect.flatMap((response) => response.json), + Effect.scoped // eliminate the `Scope` + ) +}) +``` + +#### Some apis have been renamed + +Including the `HttpClientRequest` body apis, which is to make them more +discoverable. diff --git a/packages/cluster-node/examples/sample-connect.ts b/packages/cluster-node/examples/sample-connect.ts index d030c84954..3aad0b4a60 100644 --- a/packages/cluster-node/examples/sample-connect.ts +++ b/packages/cluster-node/examples/sample-connect.ts @@ -30,25 +30,33 @@ const liveLayer = Effect.gen(function*() { Layer.effectDiscard, Layer.provide(Sharding.live), Layer.provide(StorageFile.storageFile), - Layer.provide(PodsRpc.podsRpc((podAddress) => - HttpRpcResolver.make( - HttpClient.fetchOk.pipe( - HttpClient.mapRequest( - HttpClientRequest.prependUrl(`http://${podAddress.host}:${podAddress.port}/api/rest`) - ) - ) - ).pipe(RpcResolver.toClient) - )), - Layer.provide(ShardManagerClientRpc.shardManagerClientRpc( - (shardManagerUri) => + Layer.provide(Layer.unwrapEffect(Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + return PodsRpc.podsRpc((podAddress) => HttpRpcResolver.make( - HttpClient.fetchOk.pipe( + client.pipe( + HttpClient.filterStatusOk, HttpClient.mapRequest( - HttpClientRequest.prependUrl(shardManagerUri) + HttpClientRequest.prependUrl(`http://${podAddress.host}:${podAddress.port}/api/rest`) ) ) ).pipe(RpcResolver.toClient) - )), + ) + }))), + Layer.provide(Layer.unwrapEffect(Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + return ShardManagerClientRpc.shardManagerClientRpc( + (shardManagerUri) => + HttpRpcResolver.make( + client.pipe( + HttpClient.filterStatusOk, + HttpClient.mapRequest( + HttpClientRequest.prependUrl(shardManagerUri) + ) + ) + ).pipe(RpcResolver.toClient) + ) + }))), Layer.provide(ShardingConfig.withDefaults({ shardingPort: 54322 })), Layer.provide(Serialization.json), Layer.provide(NodeHttpClient.layerUndici) diff --git a/packages/cluster-node/examples/sample-manager.ts b/packages/cluster-node/examples/sample-manager.ts index 01321024b0..571da95ae6 100644 --- a/packages/cluster-node/examples/sample-manager.ts +++ b/packages/cluster-node/examples/sample-manager.ts @@ -5,7 +5,14 @@ import * as StorageFile from "@effect/cluster-node/StorageFile" import * as ManagerConfig from "@effect/cluster/ManagerConfig" import * as PodsHealth from "@effect/cluster/PodsHealth" import * as ShardManager from "@effect/cluster/ShardManager" -import { HttpClient, HttpClientRequest, HttpMiddleware, HttpRouter, HttpServer } from "@effect/platform" +import { + FetchHttpClient, + HttpClient, + HttpClientRequest, + HttpMiddleware, + HttpRouter, + HttpServer +} from "@effect/platform" import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" import { RpcResolver } from "@effect/rpc" import { HttpRpcResolver, HttpRpcRouter } from "@effect/rpc-http" @@ -34,17 +41,21 @@ const liveShardingManager = Effect.never.pipe( Layer.provide(ShardManager.live), Layer.provide(StorageFile.storageFile), Layer.provide(PodsHealth.local), - Layer.provide(PodsRpc.podsRpc((podAddress) => - HttpRpcResolver.make( - HttpClient.fetchOk.pipe( - HttpClient.mapRequest( - HttpClientRequest.prependUrl(`http://${podAddress.host}:${podAddress.port}/api/rest`) + Layer.provide(Layer.unwrapEffect(Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + return PodsRpc.podsRpc((podAddress) => + HttpRpcResolver.make( + client.pipe( + HttpClient.filterStatusOk, + HttpClient.mapRequest( + HttpClientRequest.prependUrl(`http://${podAddress.host}:${podAddress.port}/api/rest`) + ) ) - ) - ).pipe(RpcResolver.toClient) - )), + ).pipe(RpcResolver.toClient) + ) + }))), Layer.provide(ManagerConfig.fromConfig), - Layer.provide(HttpClient.layer) + Layer.provide(FetchHttpClient.layer) ) Layer.launch(liveShardingManager).pipe( diff --git a/packages/cluster-node/examples/sample-shard.ts b/packages/cluster-node/examples/sample-shard.ts index 85854704a4..3bbfc38da3 100644 --- a/packages/cluster-node/examples/sample-shard.ts +++ b/packages/cluster-node/examples/sample-shard.ts @@ -62,25 +62,33 @@ const liveLayer = Sharding.registerEntity( Layer.provide(HttpLive), Layer.provideMerge(Sharding.live), Layer.provide(StorageFile.storageFile), - Layer.provide(PodsRpc.podsRpc((podAddress) => - HttpRpcResolver.make( - HttpClient.fetchOk.pipe( - HttpClient.mapRequest( - HttpClientRequest.prependUrl(`http://${podAddress.host}:${podAddress.port}/api/rest`) - ) - ) - ).pipe(RpcResolver.toClient) - )), - Layer.provide(ShardManagerClientRpc.shardManagerClientRpc( - (shardManagerUri) => + Layer.provide(Layer.unwrapEffect(Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + return PodsRpc.podsRpc((podAddress) => HttpRpcResolver.make( - HttpClient.fetchOk.pipe( + client.pipe( + HttpClient.filterStatusOk, HttpClient.mapRequest( - HttpClientRequest.prependUrl(shardManagerUri) + HttpClientRequest.prependUrl(`http://${podAddress.host}:${podAddress.port}/api/rest`) ) ) ).pipe(RpcResolver.toClient) - )), + ) + }))), + Layer.provide(Layer.unwrapEffect(Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + return ShardManagerClientRpc.shardManagerClientRpc( + (shardManagerUri) => + HttpRpcResolver.make( + client.pipe( + HttpClient.filterStatusOk, + HttpClient.mapRequest( + HttpClientRequest.prependUrl(shardManagerUri) + ) + ) + ).pipe(RpcResolver.toClient) + ) + }))), Layer.provide(Serialization.json), Layer.provide(NodeHttpClient.layerUndici), Layer.provide(ShardingConfig.fromConfig) diff --git a/packages/platform-browser/src/BrowserHttpClient.ts b/packages/platform-browser/src/BrowserHttpClient.ts index c633bf3407..303f59a89c 100644 --- a/packages/platform-browser/src/BrowserHttpClient.ts +++ b/packages/platform-browser/src/BrowserHttpClient.ts @@ -2,30 +2,27 @@ * @since 1.0.0 */ import type * as HttpClient from "@effect/platform/HttpClient" +import * as Context from "effect/Context" import type { Effect } from "effect/Effect" import type * as FiberRef from "effect/FiberRef" import type { LazyArg } from "effect/Function" import type * as Layer from "effect/Layer" import * as internal from "./internal/httpClient.js" -/** - * @since 1.0.0 - * @category clients - */ -export const xmlHttpRequest: HttpClient.HttpClient.Default = internal.makeXMLHttpRequest - /** * @since 1.0.0 * @category layers */ -export const layerXMLHttpRequest: Layer.Layer = - internal.layerXMLHttpRequest +export const layerXMLHttpRequest: Layer.Layer = internal.layerXMLHttpRequest /** * @since 1.0.0 - * @category fiber refs + * @category tags */ -export const currentXMLHttpRequest: FiberRef.FiberRef> = internal.currentXMLHttpRequest +export class XMLHttpRequest extends Context.Tag(internal.xhrTagKey)< + XMLHttpRequest, + LazyArg +>() {} /** * @since 1.0.0 diff --git a/packages/platform-browser/src/internal/httpClient.ts b/packages/platform-browser/src/internal/httpClient.ts index 89133041b9..b8b4b96ca9 100644 --- a/packages/platform-browser/src/internal/httpClient.ts +++ b/packages/platform-browser/src/internal/httpClient.ts @@ -6,21 +6,20 @@ import type * as ClientRequest from "@effect/platform/HttpClientRequest" import * as ClientResponse from "@effect/platform/HttpClientResponse" import * as IncomingMessage from "@effect/platform/HttpIncomingMessage" import * as UrlParams from "@effect/platform/UrlParams" +import * as Context from "effect/Context" import * as Effect from "effect/Effect" import * as FiberRef from "effect/FiberRef" import { type LazyArg } from "effect/Function" import { globalValue } from "effect/GlobalValue" import * as Inspectable from "effect/Inspectable" -import * as Layer from "effect/Layer" import * as Option from "effect/Option" import * as Stream from "effect/Stream" import * as HeaderParser from "multipasta/HeadersParser" /** @internal */ -export const currentXMLHttpRequest = globalValue( - "@effect/platform-browser/BrowserHttpClient/currentXMLHttpRequest", - () => FiberRef.unsafeMake>(() => new XMLHttpRequest()) -) +export const xhrTagKey = "@effect/platform-browser/BrowserHttpClient/XMLHttpRequest" + +const xhrTag = Context.GenericTag>(xhrTagKey) /** @internal */ export const currentXHRResponseType = globalValue( @@ -36,10 +35,15 @@ export const withXHRArrayBuffer = (effect: Effect.Effect): Eff "arraybuffer" ) -/** @internal */ -export const makeXMLHttpRequest = Client.makeDefault((request, url, signal, fiber) => +const makeXhr = () => new XMLHttpRequest() + +const makeXMLHttpRequest = Client.makeService((request, url, signal, fiber) => Effect.suspend(() => { - const xhr = fiber.getFiberRef(currentXMLHttpRequest)() + const xhr = Context.getOrElse( + fiber.getFiberRef(FiberRef.currentContext), + xhrTag, + () => makeXhr + )() signal.addEventListener("abort", () => { xhr.abort() xhr.onreadystatechange = null @@ -70,6 +74,7 @@ export const makeXMLHttpRequest = Client.makeDefault((request, url, signal, fibe )) } onChange() + return Effect.void }) ) }) @@ -332,4 +337,4 @@ class ClientResponseImpl extends IncomingMessageImpl implem } /** @internal */ -export const layerXMLHttpRequest = Layer.succeed(Client.HttpClient, makeXMLHttpRequest) +export const layerXMLHttpRequest = Client.layerMergedContext(Effect.succeed(makeXMLHttpRequest)) diff --git a/packages/platform-browser/test/BrowserHttpClient.test.ts b/packages/platform-browser/test/BrowserHttpClient.test.ts index 8e8367c278..7dab7b9c5f 100644 --- a/packages/platform-browser/test/BrowserHttpClient.test.ts +++ b/packages/platform-browser/test/BrowserHttpClient.test.ts @@ -1,39 +1,35 @@ -import { Cookies, HttpClientRequest, HttpClientResponse } from "@effect/platform" +import { Cookies, HttpClientRequest } from "@effect/platform" import { BrowserHttpClient } from "@effect/platform-browser" import { assert, describe, it } from "@effect/vitest" -import { Chunk, Effect, Stream } from "effect" +import { Chunk, Effect, Layer, Stream } from "effect" import * as MXHR from "mock-xmlhttprequest" +const layer = (...args: Parameters) => + Layer.unwrapEffect(Effect.sync(() => { + const server = MXHR.newServer(...args) + return BrowserHttpClient.layerXMLHttpRequest.pipe( + Layer.provide(Layer.succeed(BrowserHttpClient.XMLHttpRequest, server.xhrFactory)) + ) + })) + describe("BrowserHttpClient", () => { it.effect("json", () => - Effect.gen(function*(_) { - const server = MXHR.newServer({ - get: ["http://localhost:8080/my/url", { - headers: { "Content-Type": "application/json" }, - body: "{ \"message\": \"Success!\" }" - }] - }) - const body = yield* _( - HttpClientRequest.get("http://localhost:8080/my/url"), - BrowserHttpClient.xmlHttpRequest, + Effect.gen(function*() { + const body = yield* HttpClientRequest.get("http://localhost:8080/my/url").pipe( Effect.flatMap((_) => _.json), - Effect.scoped, - Effect.locally(BrowserHttpClient.currentXMLHttpRequest, server.xhrFactory) + Effect.scoped ) assert.deepStrictEqual(body, { message: "Success!" }) - })) + }).pipe(Effect.provide(layer({ + get: ["http://localhost:8080/my/url", { + headers: { "Content-Type": "application/json" }, + body: "{ \"message\": \"Success!\" }" + }] + })))) it.effect("stream", () => - Effect.gen(function*(_) { - const server = MXHR.newServer({ - get: ["http://localhost:8080/my/url", { - headers: { "Content-Type": "application/json" }, - body: "{ \"message\": \"Success!\" }" - }] - }) - const body = yield* _( - HttpClientRequest.get("http://localhost:8080/my/url"), - BrowserHttpClient.xmlHttpRequest, + Effect.gen(function*() { + const body = yield* HttpClientRequest.get("http://localhost:8080/my/url").pipe( Effect.map((_) => _.stream.pipe( Stream.decodeText(), @@ -41,47 +37,48 @@ describe("BrowserHttpClient", () => { ) ), Stream.unwrapScoped, - Stream.runCollect, - Effect.locally(BrowserHttpClient.currentXMLHttpRequest, server.xhrFactory) + Stream.runCollect ) assert.deepStrictEqual(Chunk.unsafeHead(body), "{ \"message\": \"Success!\" }") - })) + }).pipe(Effect.provide(layer({ + get: ["http://localhost:8080/my/url", { + headers: { "Content-Type": "application/json" }, + body: "{ \"message\": \"Success!\" }" + }] + })))) it.effect("cookies", () => - Effect.gen(function*(_) { - const server = MXHR.newServer({ - get: ["http://localhost:8080/my/url", { - headers: { "Content-Type": "application/json", "Set-Cookie": "foo=bar; HttpOnly; Secure" }, - body: "{ \"message\": \"Success!\" }" - }] - }) - const cookies = yield* _( - HttpClientRequest.get("http://localhost:8080/my/url"), - BrowserHttpClient.xmlHttpRequest, + Effect.gen(function*() { + const cookies = yield* HttpClientRequest.get("http://localhost:8080/my/url").pipe( Effect.map((res) => res.cookies), - Effect.scoped, - Effect.locally(BrowserHttpClient.currentXMLHttpRequest, server.xhrFactory) + Effect.scoped ) assert.deepStrictEqual(Cookies.toRecord(cookies), { foo: "bar" }) - })) + }).pipe( + Effect.provide(layer({ + get: ["http://localhost:8080/my/url", { + headers: { "Content-Type": "application/json", "Set-Cookie": "foo=bar; HttpOnly; Secure" }, + body: "{ \"message\": \"Success!\" }" + }] + })) + )) it.effect("arrayBuffer", () => - Effect.gen(function*(_) { - const server = MXHR.newServer({ + Effect.gen(function*() { + const body = yield* HttpClientRequest.get("http://localhost:8080/my/url").pipe( + Effect.flatMap((_) => _.arrayBuffer), + Effect.scoped, + BrowserHttpClient.withXHRArrayBuffer + ) + assert.strictEqual(new TextDecoder().decode(body), "{ \"message\": \"Success!\" }") + }).pipe( + Effect.provide(layer({ get: ["http://localhost:8080/my/url", { headers: { "Content-Type": "application/json" }, body: "{ \"message\": \"Success!\" }" }] - }) - const body = yield* _( - HttpClientRequest.get("http://localhost:8080/my/url"), - BrowserHttpClient.xmlHttpRequest, - HttpClientResponse.arrayBuffer, - BrowserHttpClient.withXHRArrayBuffer, - Effect.locally(BrowserHttpClient.currentXMLHttpRequest, server.xhrFactory) - ) - assert.strictEqual(new TextDecoder().decode(body), "{ \"message\": \"Success!\" }") - })) + })) + )) }) diff --git a/packages/platform-bun/examples/http-client.ts b/packages/platform-bun/examples/http-client.ts index 20d3866f72..eeaab80b82 100644 --- a/packages/platform-bun/examples/http-client.ts +++ b/packages/platform-bun/examples/http-client.ts @@ -1,4 +1,4 @@ -import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" +import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" import type { HttpBody, HttpClientError } from "@effect/platform" import { BunRuntime } from "@effect/platform-bun" import type * as ParseResult from "@effect/schema/ParseResult" @@ -29,21 +29,22 @@ const makeTodoService = Effect.gen(function*() { HttpClient.mapRequest(HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com")) ) - const addTodoWithoutIdBody = HttpClientRequest.schemaBody(TodoWithoutId) + const addTodoWithoutIdBody = HttpClientRequest.schemaBodyJson(TodoWithoutId) const create = (todo: TodoWithoutId) => addTodoWithoutIdBody( HttpClientRequest.post("/todos"), todo ).pipe( - Effect.flatMap(clientWithBaseUrl), - HttpClientResponse.schemaBodyJsonScoped(Todo) + Effect.flatMap(clientWithBaseUrl.execute), + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), + Effect.scoped ) return TodoService.of({ create }) }) const TodoServiceLive = Layer.effect(TodoService, makeTodoService).pipe( - Layer.provide(HttpClient.layer) + Layer.provide(FetchHttpClient.layer) ) Effect.flatMap( diff --git a/packages/platform-bun/src/BunHttpServer.ts b/packages/platform-bun/src/BunHttpServer.ts index 99e8b54239..913dec2972 100644 --- a/packages/platform-bun/src/BunHttpServer.ts +++ b/packages/platform-bun/src/BunHttpServer.ts @@ -46,7 +46,7 @@ export const layer: ( * @category layers */ export const layerTest: Layer.Layer< - | HttpClient.HttpClient.Default + | HttpClient.HttpClient.Service | Server.HttpServer | Platform.HttpPlatform | Etag.Generator diff --git a/packages/platform-bun/src/internal/httpServer.ts b/packages/platform-bun/src/internal/httpServer.ts index 731608133e..f4169b4bfb 100644 --- a/packages/platform-bun/src/internal/httpServer.ts +++ b/packages/platform-bun/src/internal/httpServer.ts @@ -2,10 +2,10 @@ import * as MultipartNode from "@effect/platform-node-shared/NodeMultipart" import * as Cookies from "@effect/platform/Cookies" import * as Etag from "@effect/platform/Etag" +import * as FetchHttpClient from "@effect/platform/FetchHttpClient" import type * as FileSystem from "@effect/platform/FileSystem" import * as Headers from "@effect/platform/Headers" import * as App from "@effect/platform/HttpApp" -import * as HttpClient from "@effect/platform/HttpClient" import * as IncomingMessage from "@effect/platform/HttpIncomingMessage" import type { HttpMethod } from "@effect/platform/HttpMethod" import * as Server from "@effect/platform/HttpServer" @@ -180,11 +180,8 @@ export const layer = ( /** @internal */ export const layerTest = Server.layerTestClient.pipe( - Layer.provide(Layer.succeed( - HttpClient.HttpClient, - HttpClient.fetch.pipe( - HttpClient.transformResponse(HttpClient.withFetchOptions({ keepalive: false })) - ) + Layer.provide(FetchHttpClient.layer.pipe( + Layer.provide(Layer.succeed(FetchHttpClient.RequestInit, { keepalive: false })) )), Layer.provideMerge(layer({ port: 0 })) ) diff --git a/packages/platform-node/examples/api.ts b/packages/platform-node/examples/api.ts index e754b46ccb..31f9da08fd 100644 --- a/packages/platform-node/examples/api.ts +++ b/packages/platform-node/examples/api.ts @@ -1,4 +1,5 @@ import { + FetchHttpClient, HttpApi, HttpApiBuilder, HttpApiClient, @@ -7,7 +8,6 @@ import { HttpApiSchema, HttpApiSecurity, HttpApiSwagger, - HttpClient, HttpMiddleware, HttpServer, OpenApi @@ -173,6 +173,6 @@ Effect.gen(function*() { const binary = yield* client.users.binary() console.log("binary", binary) }).pipe( - Effect.provide(HttpClient.layer), + Effect.provide(FetchHttpClient.layer), NodeRuntime.runMain ) diff --git a/packages/platform-node/examples/http-client.ts b/packages/platform-node/examples/http-client.ts index 840c493d45..abcaf80891 100644 --- a/packages/platform-node/examples/http-client.ts +++ b/packages/platform-node/examples/http-client.ts @@ -14,7 +14,7 @@ class Todo extends Schema.Class("Todo")({ title: Schema.String, completed: Schema.Boolean }) { - static decodeResponse = HttpClientResponse.schemaBodyJsonScoped(Todo) + static decodeResponse = HttpClientResponse.schemaBodyJson(Todo) } const TodoWithoutId = Schema.Struct(Todo.fields).pipe(Schema.omit("id")) @@ -34,14 +34,15 @@ const makeTodoService = Effect.gen(function*() { HttpClient.mapRequest(HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com")) ) - const addTodoWithoutIdBody = HttpClientRequest.schemaBody(TodoWithoutId) + const addTodoWithoutIdBody = HttpClientRequest.schemaBodyJson(TodoWithoutId) const create = (todo: TodoWithoutId) => addTodoWithoutIdBody( HttpClientRequest.post("/todos"), todo ).pipe( - Effect.flatMap(clientWithBaseUrl), - Todo.decodeResponse + Effect.flatMap(clientWithBaseUrl.execute), + Effect.flatMap(Todo.decodeResponse), + Effect.scoped ) return TodoService.of({ create }) diff --git a/packages/platform-node/src/NodeHttpClient.ts b/packages/platform-node/src/NodeHttpClient.ts index bbf5e64ba7..d1d2657577 100644 --- a/packages/platform-node/src/NodeHttpClient.ts +++ b/packages/platform-node/src/NodeHttpClient.ts @@ -2,9 +2,8 @@ * @since 1.0.0 */ import type * as Client from "@effect/platform/HttpClient" -import type * as Context from "effect/Context" +import * as Context from "effect/Context" import type * as Effect from "effect/Effect" -import type * as FiberRef from "effect/FiberRef" import type * as Layer from "effect/Layer" import type * as Scope from "effect/Scope" import type * as Http from "node:http" @@ -64,19 +63,19 @@ export const makeAgentLayer: (options?: Https.AgentOptions) => Layer.Layer = internal.make +export const make: Effect.Effect = internal.make /** * @since 1.0.0 * @category layers */ -export const layer: Layer.Layer = internal.layer +export const layer: Layer.Layer = internal.layer /** * @since 1.0.0 * @category layers */ -export const layerWithoutAgent: Layer.Layer = internal.layerWithoutAgent +export const layerWithoutAgent: Layer.Layer = internal.layerWithoutAgent /** * @since 1.0.0 @@ -114,35 +113,26 @@ export const dispatcherLayerGlobal: Layer.Layer = internalUndici.dis * @since 1.0.0 * @category undici */ -export const currentUndiciOptions: FiberRef.FiberRef> = - internalUndici.currentUndiciOptions - -/** - * @since 1.0.0 - * @category undici - */ -export const withUndiciOptions: { - ( - options: Partial - ): (effect: Effect.Effect) => Effect.Effect - (effect: Effect.Effect, options: Partial): Effect.Effect -} = internalUndici.withUndiciOptions +export class UndiciRequestOptions extends Context.Tag(internalUndici.undiciOptionsTagKey)< + UndiciRequestOptions, + Undici.Dispatcher.RequestOptions +>() {} /** * @since 1.0.0 * @category constructors */ -export const makeUndici: (dispatcher: Undici.Dispatcher) => Client.HttpClient.Default = internalUndici.make +export const makeUndici: (dispatcher: Undici.Dispatcher) => Client.HttpClient.Service = internalUndici.make /** * @since 1.0.0 * @category layers */ -export const layerUndici: Layer.Layer = internalUndici.layer +export const layerUndici: Layer.Layer = internalUndici.layer /** * @since 1.0.0 * @category layers */ -export const layerUndiciWithoutDispatcher: Layer.Layer = +export const layerUndiciWithoutDispatcher: Layer.Layer = internalUndici.layerWithoutDispatcher diff --git a/packages/platform-node/src/NodeHttpServer.ts b/packages/platform-node/src/NodeHttpServer.ts index c57f2e452e..244ecc0ee5 100644 --- a/packages/platform-node/src/NodeHttpServer.ts +++ b/packages/platform-node/src/NodeHttpServer.ts @@ -105,7 +105,7 @@ export const layerConfig: ( * @category layers */ export const layerTest: Layer.Layer< - | HttpClient.HttpClient.Default + | HttpClient.HttpClient.Service | Server.HttpServer | Platform.HttpPlatform | Etag.Generator diff --git a/packages/platform-node/src/internal/httpClient.ts b/packages/platform-node/src/internal/httpClient.ts index 1d1187c8c6..3b7104fc34 100644 --- a/packages/platform-node/src/internal/httpClient.ts +++ b/packages/platform-node/src/internal/httpClient.ts @@ -54,8 +54,8 @@ export const makeAgentLayer = (options?: Https.AgentOptions): Layer.Layer - Client.makeDefault((request, url, signal) => { +const fromAgent = (agent: NodeClient.HttpAgent): Client.HttpClient.Service => + Client.makeService((request, url, signal) => { const nodeRequest = url.protocol === "https:" ? Https.request(url, { agent: agent.https, @@ -254,7 +254,7 @@ class ClientResponseImpl extends HttpIncomingMessageImpl export const make = Effect.map(HttpAgent, fromAgent) /** @internal */ -export const layerWithoutAgent = Layer.effect(Client.HttpClient, make) +export const layerWithoutAgent = Client.layerMergedContext(make) /** @internal */ export const layer = Layer.provide(layerWithoutAgent, agentLayer) diff --git a/packages/platform-node/src/internal/httpClientUndici.ts b/packages/platform-node/src/internal/httpClientUndici.ts index d83aa640cf..7574f3f7d3 100644 --- a/packages/platform-node/src/internal/httpClientUndici.ts +++ b/packages/platform-node/src/internal/httpClientUndici.ts @@ -10,8 +10,6 @@ import * as UrlParams from "@effect/platform/UrlParams" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import * as FiberRef from "effect/FiberRef" -import { dual } from "effect/Function" -import { globalValue } from "effect/GlobalValue" import * as Inspectable from "effect/Inspectable" import * as Layer from "effect/Layer" import * as Option from "effect/Option" @@ -39,31 +37,19 @@ export const dispatcherLayer = Layer.scoped(Dispatcher, makeDispatcher) export const dispatcherLayerGlobal = Layer.sync(Dispatcher, () => Undici.getGlobalDispatcher()) /** @internal */ -export const currentUndiciOptions = globalValue( - Symbol.for("@effect/platform-node/NodeHttpClient/currentUndici"), - () => FiberRef.unsafeMake>({}) -) - -/** @internal */ -export const withUndiciOptions = dual< - ( - options: Partial - ) => (effect: Effect.Effect) => Effect.Effect, - ( - effect: Effect.Effect, - options: Partial - ) => Effect.Effect ->(2, (self, options) => Effect.locally(self, currentUndiciOptions, options)) +export const undiciOptionsTagKey = "@effect/platform-node/NodeHttpClient/undiciOptions" /** @internal */ -export const make = (dispatcher: Undici.Dispatcher): Client.HttpClient.Default => - Client.makeDefault((request, url, signal, fiber) => - convertBody(request.body).pipe( +export const make = (dispatcher: Undici.Dispatcher): Client.HttpClient.Service => + Client.makeService((request, url, signal, fiber) => { + const context = fiber.getFiberRef(FiberRef.currentContext) + const options: Undici.Dispatcher.RequestOptions = context.unsafeMap.get(undiciOptionsTagKey) ?? {} + return convertBody(request.body).pipe( Effect.flatMap((body) => Effect.tryPromise({ try: () => dispatcher.request({ - ...(fiber.getFiberRef(currentUndiciOptions)), + ...options, signal, method: request.method, headers: request.headers, @@ -85,7 +71,7 @@ export const make = (dispatcher: Undici.Dispatcher): Client.HttpClient.Default = ), Effect.map((response) => new ClientResponseImpl(request, response)) ) - ) + }) function convertBody( body: Body.HttpBody @@ -240,7 +226,7 @@ class ClientResponseImpl extends Inspectable.Class implements ClientResponse.Htt } /** @internal */ -export const layerWithoutDispatcher = Layer.effect(Client.HttpClient, Effect.map(Dispatcher, make)) +export const layerWithoutDispatcher = Client.layerMergedContext(Effect.map(Dispatcher, make)) /** @internal */ export const layer = Layer.provide(layerWithoutDispatcher, dispatcherLayer) diff --git a/packages/platform-node/test/HttpApi.test.ts b/packages/platform-node/test/HttpApi.test.ts index 4455195759..847470279f 100644 --- a/packages/platform-node/test/HttpApi.test.ts +++ b/packages/platform-node/test/HttpApi.test.ts @@ -103,7 +103,7 @@ describe("HttpApi", () => { it.scoped("class level annotations", () => Effect.gen(function*() { const response = yield* HttpClientRequest.post("/users").pipe( - HttpClientRequest.unsafeJsonBody({ name: "boom" }) + HttpClientRequest.bodyUnsafeJson({ name: "boom" }) ) assert.strictEqual(response.status, 400) }).pipe(Effect.provide(HttpLive))) diff --git a/packages/platform-node/test/HttpClient.test.ts b/packages/platform-node/test/HttpClient.test.ts index 628fdb347a..574d4a279d 100644 --- a/packages/platform-node/test/HttpClient.test.ts +++ b/packages/platform-node/test/HttpClient.test.ts @@ -20,7 +20,8 @@ const makeJsonPlaceholder = Effect.gen(function*(_) { HttpClient.mapRequest(HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com")) ) const todoClient = client.pipe( - HttpClient.mapEffectScoped(HttpClientResponse.schemaBodyJson(Todo)) + HttpClient.mapEffect(HttpClientResponse.schemaBodyJson(Todo)), + HttpClient.scoped ) const createTodo = HttpClient.schemaFunction( todoClient, @@ -48,10 +49,8 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) describe(`NodeHttpClient - ${name}`, () => { it.effect("google", () => Effect.gen(function*(_) { - const client = yield* _(HttpClient.HttpClient) const response = yield* _( HttpClientRequest.get("https://www.google.com/"), - client, Effect.flatMap((_) => _.text), Effect.scoped ) @@ -59,13 +58,11 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) }).pipe(Effect.provide(layer))) it.effect("google followRedirects", () => - Effect.gen(function*(_) { + Effect.gen(function*() { const client = (yield* HttpClient.HttpClient).pipe( HttpClient.followRedirects() ) - const response = yield* _( - HttpClientRequest.get("http://google.com/"), - client, + const response = yield* client.get("http://google.com/").pipe( Effect.flatMap((_) => _.text), Effect.scoped ) @@ -73,11 +70,9 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) }).pipe(Effect.provide(layer))) it.effect("google stream", () => - Effect.gen(function*(_) { - const client = yield* _(HttpClient.HttpClient) - const response = yield* _( - HttpClientRequest.get("https://www.google.com/"), - client, + Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + const response = yield* client.get("https://www.google.com/").pipe( Effect.map((_) => _.stream), Stream.unwrapScoped, Stream.runFold("", (a, b) => a + new TextDecoder().decode(b)) @@ -86,45 +81,45 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) }).pipe(Effect.provide(layer))) it.effect("jsonplaceholder", () => - Effect.gen(function*(_) { - const jp = yield* _(JsonPlaceholder) - const response = yield* _(HttpClientRequest.get("/todos/1"), jp.todoClient) + Effect.gen(function*() { + const jp = yield* JsonPlaceholder + const response = yield* jp.todoClient.get("/todos/1") expect(response.id).toBe(1) }).pipe(Effect.provide(JsonPlaceholderLive.pipe( Layer.provide(layer) )))) it.effect("jsonplaceholder schemaFunction", () => - Effect.gen(function*(_) { - const jp = yield* _(JsonPlaceholder) - const response = yield* _(jp.createTodo({ + Effect.gen(function*() { + const jp = yield* JsonPlaceholder + const response = yield* jp.createTodo({ userId: 1, title: "test", completed: false - })) + }) expect(response.title).toBe("test") }).pipe(Effect.provide(JsonPlaceholderLive.pipe( Layer.provide(layer) )))) it.effect("head request with schemaJson", () => - Effect.gen(function*(_) { - const client = yield* _(HttpClient.HttpClient) - const response = yield* _( - HttpClientRequest.head("https://jsonplaceholder.typicode.com/todos"), - client, - HttpClientResponse.schemaJsonScoped(Schema.Struct({ status: Schema.Literal(200) })) + Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + const response = yield* client.head("https://jsonplaceholder.typicode.com/todos").pipe( + Effect.flatMap( + HttpClientResponse.schemaJson(Schema.Struct({ status: Schema.Literal(200) })) + ), + Effect.scoped ) expect(response).toEqual({ status: 200 }) }).pipe(Effect.provide(layer))) it.live("interrupt", () => - Effect.gen(function*(_) { - const client = yield* _(HttpClient.HttpClient) - const response = yield* _( - HttpClientRequest.get("https://www.google.com/"), - client, - HttpClientResponse.text, + Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + const response = yield* client.get("https://www.google.com/").pipe( + Effect.flatMap((_) => _.text), + Effect.scoped, Effect.timeout(1), Effect.asSome, Effect.catchTag("TimeoutException", () => Effect.succeedNone) diff --git a/packages/platform-node/test/HttpServer.test.ts b/packages/platform-node/test/HttpServer.test.ts index 4ff8f94e14..0afd0556c3 100644 --- a/packages/platform-node/test/HttpServer.test.ts +++ b/packages/platform-node/test/HttpServer.test.ts @@ -18,7 +18,7 @@ import { import { NodeHttpServer } from "@effect/platform-node" import * as Schema from "@effect/schema/Schema" import { assert, describe, expect, it } from "@effect/vitest" -import { Deferred, Duration, Fiber, Stream } from "effect" +import { Deferred, Duration, Fiber, flow, Stream } from "effect" import * as Effect from "effect/Effect" import * as Option from "effect/Option" import * as Tracer from "effect/Tracer" @@ -35,16 +35,16 @@ const todoResponse = HttpServerResponse.schemaJson(Todo) const makeTodoClient = Effect.map( HttpClient.HttpClient, - HttpClient.mapEffectScoped( - HttpClientResponse.schemaBodyJson(Todo) + flow( + HttpClient.mapEffect(HttpClientResponse.schemaBodyJson(Todo)), + HttpClient.scoped ) ) describe("HttpServer", () => { it.scoped("schema", () => - Effect.gen(function*(_) { - yield* _( - HttpRouter.empty, + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( HttpRouter.get( "/todos/:id", Effect.flatMap( @@ -54,26 +54,25 @@ describe("HttpServer", () => { ), HttpServer.serveEffect() ) - const client = yield* _(makeTodoClient) - const todo = yield* _(client(HttpClientRequest.get("/todos/1"))) + const client = yield* makeTodoClient + const todo = yield* client.get("/todos/1") expect(todo).toEqual({ id: 1, title: "test" }) }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("formData", () => - Effect.gen(function*(_) { - yield* _( - HttpRouter.empty, + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( HttpRouter.post( "/upload", - Effect.gen(function*(_) { - const request = yield* _(HttpServerRequest.HttpServerRequest) - const formData = yield* _(request.multipart) + Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + const formData = yield* request.multipart const part = formData.file assert(typeof part !== "string") const file = part[0] expect(file.path.endsWith("/test.txt")).toEqual(true) expect(file.contentType).toEqual("text/plain") - return yield* _(HttpServerResponse.json({ ok: "file" in formData })) + return yield* HttpServerResponse.json({ ok: "file" in formData }) }) ), HttpServer.serveEffect() @@ -81,24 +80,23 @@ describe("HttpServer", () => { const client = yield* HttpClient.HttpClient const formData = new FormData() formData.append("file", new Blob(["test"], { type: "text/plain" }), "test.txt") - const result = yield* _( - client(HttpClientRequest.post("/upload", { body: HttpBody.formData(formData) })), - HttpClientResponse.json + const result = yield* client.post("/upload", { body: HttpBody.formData(formData) }).pipe( + Effect.flatMap((r) => r.json), + Effect.scoped ) expect(result).toEqual({ ok: true }) }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("schemaBodyForm", () => - Effect.gen(function*(_) { - yield* _( - HttpRouter.empty, + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( HttpRouter.post( "/upload", - Effect.gen(function*(_) { - const files = yield* _(HttpServerRequest.schemaBodyForm(Schema.Struct({ + Effect.gen(function*() { + const files = yield* HttpServerRequest.schemaBodyForm(Schema.Struct({ file: Multipart.FilesSchema, test: Schema.String - }))) + })) expect(files).toHaveProperty("file") expect(files).toHaveProperty("test") return HttpServerResponse.empty() @@ -111,22 +109,20 @@ describe("HttpServer", () => { const formData = new FormData() formData.append("file", new Blob(["test"], { type: "text/plain" }), "test.txt") formData.append("test", "test") - const response = yield* _( - client(HttpClientRequest.post("/upload", { body: HttpBody.formData(formData) })), + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }).pipe( Effect.scoped ) expect(response.status).toEqual(204) }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("formData withMaxFileSize", () => - Effect.gen(function*(_) { - yield* _( - HttpRouter.empty, + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( HttpRouter.post( "/upload", - Effect.gen(function*(_) { - const request = yield* _(HttpServerRequest.HttpServerRequest) - yield* _(request.multipart) + Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + yield* request.multipart return HttpServerResponse.empty() }) ), @@ -141,22 +137,20 @@ describe("HttpServer", () => { const formData = new FormData() const data = new Uint8Array(1000) formData.append("file", new Blob([data], { type: "text/plain" }), "test.txt") - const response = yield* _( - client(HttpClientRequest.post("/upload", { body: HttpBody.formData(formData) })), + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }).pipe( Effect.scoped ) expect(response.status).toEqual(413) }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("formData withMaxFieldSize", () => - Effect.gen(function*(_) { - yield* _( - HttpRouter.empty, + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( HttpRouter.post( "/upload", - Effect.gen(function*(_) { - const request = yield* _(HttpServerRequest.HttpServerRequest) - yield* _(request.multipart) + Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + yield* request.multipart return HttpServerResponse.empty() }) ), @@ -171,51 +165,48 @@ describe("HttpServer", () => { const formData = new FormData() const data = new Uint8Array(1000).fill(1) formData.append("file", new TextDecoder().decode(data)) - const response = yield* _( - client(HttpClientRequest.post("/upload", { body: HttpBody.formData(formData) })), + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }).pipe( Effect.scoped ) expect(response.status).toEqual(413) }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("mount", () => - Effect.gen(function*(_) { + Effect.gen(function*() { const child = HttpRouter.empty.pipe( HttpRouter.get("/", Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url))), HttpRouter.get("/:id", Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url))) ) - yield* _( - HttpRouter.empty, + yield* HttpRouter.empty.pipe( HttpRouter.mount("/child", child), HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const todo = yield* _(client(HttpClientRequest.get("/child/1")), Effect.flatMap((_) => _.text), Effect.scoped) + const todo = yield* client.get("/child/1").pipe(Effect.flatMap((_) => _.text), Effect.scoped) expect(todo).toEqual("/1") - const root = yield* _(client(HttpClientRequest.get("/child")), Effect.flatMap((_) => _.text), Effect.scoped) + const root = yield* client.get("/child").pipe(Effect.flatMap((_) => _.text), Effect.scoped) expect(root).toEqual("/") }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("mountApp", () => - Effect.gen(function*(_) { + Effect.gen(function*() { const child = HttpRouter.empty.pipe( HttpRouter.get("/", Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url))), HttpRouter.get("/:id", Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url))) ) - yield* _( - HttpRouter.empty, + yield* HttpRouter.empty.pipe( HttpRouter.mountApp("/child", child), HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const todo = yield* _(client(HttpClientRequest.get("/child/1")), HttpClientResponse.text) + const todo = yield* client.get("/child/1").pipe(Effect.flatMap((_) => _.text), Effect.scoped) expect(todo).toEqual("/1") - const root = yield* _(client(HttpClientRequest.get("/child")), HttpClientResponse.text) + const root = yield* client.get("/child").pipe(Effect.flatMap((_) => _.text), Effect.scoped) expect(root).toEqual("/") }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("mountApp/includePrefix", () => - Effect.gen(function*(_) { + Effect.gen(function*() { const child = HttpRouter.empty.pipe( HttpRouter.get( "/child/", @@ -226,60 +217,56 @@ describe("HttpServer", () => { Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url)) ) ) - yield* _( - HttpRouter.empty, + yield* HttpRouter.empty.pipe( HttpRouter.mountApp("/child", child, { includePrefix: true }), HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const todo = yield* _(client(HttpClientRequest.get("/child/1")), HttpClientResponse.text) + const todo = yield* client.get("/child/1").pipe(Effect.flatMap((_) => _.text), Effect.scoped) expect(todo).toEqual("/child/1") - const root = yield* _(client(HttpClientRequest.get("/child")), HttpClientResponse.text) + const root = yield* client.get("/child").pipe(Effect.flatMap((_) => _.text), Effect.scoped) expect(root).toEqual("/child") }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("file", () => - Effect.gen(function*(_) { - yield* _( - yield* _( - HttpServerResponse.file(`${__dirname}/fixtures/text.txt`), - Effect.updateService( - HttpPlatform.HttpPlatform, - (_) => ({ - ..._, - fileResponse: (path, options) => - Effect.map( - _.fileResponse(path, options), - (res) => { - ;(res as any).headers.etag = "\"etag\"" - return res - } - ) - }) - ) - ), + Effect.gen(function*() { + yield* (yield* HttpServerResponse.file(`${__dirname}/fixtures/text.txt`).pipe( + Effect.updateService( + HttpPlatform.HttpPlatform, + (_) => ({ + ..._, + fileResponse: (path, options) => + Effect.map( + _.fileResponse(path, options), + (res) => { + ;(res as any).headers.etag = "\"etag\"" + return res + } + ) + }) + ) + )).pipe( Effect.tapErrorCause(Effect.logError), HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const res = yield* _(client(HttpClientRequest.get("/")), Effect.scoped) + const res = yield* client.get("/").pipe(Effect.scoped) expect(res.status).toEqual(200) expect(res.headers["content-type"]).toEqual("text/plain") expect(res.headers["content-length"]).toEqual("27") expect(res.headers.etag).toEqual("\"etag\"") - const text = yield* _(res.text) + const text = yield* res.text expect(text.trim()).toEqual("lorem ipsum dolar sit amet") }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("fileWeb", () => - Effect.gen(function*(_) { + Effect.gen(function*() { const now = new Date() const file = new Buffer.File([new TextEncoder().encode("test")], "test.txt", { type: "text/plain", lastModified: now.getTime() }) - yield* _( - HttpServerResponse.fileWeb(file), + yield* HttpServerResponse.fileWeb(file).pipe( Effect.updateService( HttpPlatform.HttpPlatform, (_) => ({ @@ -294,20 +281,19 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const res = yield* _(client(HttpClientRequest.get("/")), Effect.scoped) + const res = yield* client.get("/").pipe(Effect.scoped) expect(res.status).toEqual(200) expect(res.headers["content-type"]).toEqual("text/plain") expect(res.headers["content-length"]).toEqual("4") expect(res.headers["last-modified"]).toEqual(now.toUTCString()) expect(res.headers.etag).toEqual("W/\"etag\"") - const text = yield* _(res.text) + const text = yield* res.text expect(text.trim()).toEqual("test") }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("schemaBodyUrlParams", () => - Effect.gen(function*(_) { - yield* _( - HttpRouter.empty, + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( HttpRouter.post( "/todos", Effect.flatMap( @@ -320,20 +306,18 @@ describe("HttpServer", () => { ), HttpServer.serveEffect() ) - const client = yield* _(makeTodoClient) - const todo = yield* _( - HttpClientRequest.post("/todos"), - HttpClientRequest.urlParamsBody({ id: "1", title: "test" }), - client, + const client = yield* makeTodoClient + const todo = yield* HttpClientRequest.post("/todos").pipe( + HttpClientRequest.bodyUrlParams({ id: "1", title: "test" }), + client.execute, Effect.scoped ) expect(todo).toEqual({ id: 1, title: "test" }) }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("schemaBodyUrlParams error", () => - Effect.gen(function*(_) { - yield* _( - HttpRouter.empty, + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( HttpRouter.get( "/todos", Effect.flatMap( @@ -348,26 +332,19 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const response = yield* _( - HttpClientRequest.get("/todos"), - client, - Effect.scoped - ) + const response = yield* client.get("/todos").pipe(Effect.scoped) expect(response.status).toEqual(400) }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("schemaBodyFormJson", () => - Effect.gen(function*(_) { - yield* _( - HttpRouter.empty, + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( HttpRouter.post( "/upload", - Effect.gen(function*(_) { - const result = yield* _( - HttpServerRequest.schemaBodyFormJson(Schema.Struct({ - test: Schema.String - }))("json") - ) + Effect.gen(function*() { + const result = yield* HttpServerRequest.schemaBodyFormJson(Schema.Struct({ + test: Schema.String + }))("json") expect(result.test).toEqual("content") return HttpServerResponse.empty() }) @@ -378,25 +355,20 @@ describe("HttpServer", () => { const client = yield* HttpClient.HttpClient const formData = new FormData() formData.append("json", JSON.stringify({ test: "content" })) - const response = yield* _( - client(HttpClientRequest.post("/upload", { body: HttpBody.formData(formData) })), - Effect.scoped - ) + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }).pipe(Effect.scoped) expect(response.status).toEqual(204) }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("schemaBodyFormJson file", () => - Effect.gen(function*(_) { - yield* _( - HttpRouter.empty, + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( HttpRouter.post( "/upload", - Effect.gen(function*(_) { - const result = yield* _( - HttpServerRequest.schemaBodyFormJson(Schema.Struct({ - test: Schema.String - }))("json") - ) + Effect.gen(function*() { + const result = yield* HttpServerRequest.schemaBodyFormJson(Schema.Struct({ + test: Schema.String + }))("json") + expect(result.test).toEqual("content") return HttpServerResponse.empty() }) @@ -411,25 +383,19 @@ describe("HttpServer", () => { new Blob([JSON.stringify({ test: "content" })], { type: "application/json" }), "test.json" ) - const response = yield* _( - client(HttpClientRequest.post("/upload", { body: HttpBody.formData(formData) })), - Effect.scoped - ) + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }).pipe(Effect.scoped) expect(response.status).toEqual(204) }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("schemaBodyFormJson url encoded", () => - Effect.gen(function*(_) { - yield* _( - HttpRouter.empty, + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( HttpRouter.post( "/upload", - Effect.gen(function*(_) { - const result = yield* _( - HttpServerRequest.schemaBodyFormJson(Schema.Struct({ - test: Schema.String - }))("json") - ) + Effect.gen(function*() { + const result = yield* HttpServerRequest.schemaBodyFormJson(Schema.Struct({ + test: Schema.String + }))("json") expect(result.test).toEqual("content") return HttpServerResponse.empty() }) @@ -438,23 +404,17 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const response = yield* _( - client( - HttpClientRequest.post("/upload", { - body: HttpBody.urlParams(UrlParams.fromInput({ - json: JSON.stringify({ test: "content" }) - })) - }) - ), - Effect.scoped - ) + const response = yield* client.post("/upload", { + body: HttpBody.urlParams(UrlParams.fromInput({ + json: JSON.stringify({ test: "content" }) + })) + }).pipe(Effect.scoped) expect(response.status).toEqual(204) }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("tracing", () => - Effect.gen(function*(_) { - yield* _( - HttpRouter.empty, + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( HttpRouter.get( "/", Effect.flatMap( @@ -465,10 +425,10 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const requestSpan = yield* _(Effect.makeSpan("client request")) - const body = yield* _( - client(HttpClientRequest.get("/")), - HttpClientResponse.json, + const requestSpan = yield* Effect.makeSpan("client request") + const body = yield* client.get("/").pipe( + Effect.flatMap((r) => r.json), + Effect.scoped, Effect.withTracer(Tracer.make({ span(name, parent, _, __, ___, kind) { assert.strictEqual(name, "http.client GET") @@ -488,27 +448,25 @@ describe("HttpServer", () => { }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scopedLive("client abort", () => - Effect.gen(function*(_) { - const latch = yield* _(Deferred.make()) - yield* _( - HttpServerResponse.empty(), + Effect.gen(function*() { + const latch = yield* Deferred.make() + yield* HttpServerResponse.empty().pipe( Effect.delay(1000), Effect.interruptible, HttpServer.serveEffect((app) => Effect.onExit(app, (exit) => Deferred.complete(latch, exit))) ) const client = yield* HttpClient.HttpClient - const fiber = yield* _(client(HttpClientRequest.get("/")), Effect.scoped, Effect.fork) - yield* _(Effect.sleep(100)) - yield* _(Fiber.interrupt(fiber)) - const cause = yield* _(Deferred.await(latch), Effect.sandbox, Effect.flip) + const fiber = yield* client.get("/").pipe(Effect.scoped, Effect.fork) + yield* Effect.sleep(100) + yield* Fiber.interrupt(fiber) + const cause = yield* Deferred.await(latch).pipe(Effect.sandbox, Effect.flip) const [response] = HttpServerError.causeResponseStripped(cause) expect(response.status).toEqual(499) }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("multiplex", () => - Effect.gen(function*(_) { - yield* _( - HttpMultiplex.empty, + Effect.gen(function*() { + yield* HttpMultiplex.empty.pipe( HttpMultiplex.hostExact("a.example.com", HttpServerResponse.text("A")), HttpMultiplex.hostStartsWith("b.", HttpServerResponse.text("B")), HttpMultiplex.hostRegex(/^c\.example/, HttpServerResponse.text("C")), @@ -516,51 +474,50 @@ describe("HttpServer", () => { ) const client = yield* HttpClient.HttpClient expect( - yield* _( - client( - HttpClientRequest.get("/").pipe( - HttpClientRequest.setHeader("host", "a.example.com") - ) - ), - HttpClientResponse.text + yield* client.execute( + HttpClientRequest.get("/").pipe( + HttpClientRequest.setHeader("host", "a.example.com") + ) + ).pipe( + Effect.flatMap((r) => r.text), + Effect.scoped ) ).toEqual("A") expect( - yield* _( - client( - HttpClientRequest.get("/").pipe( - HttpClientRequest.setHeader("host", "b.example.com") - ) - ), - HttpClientResponse.text + yield* client.execute( + HttpClientRequest.get("/").pipe( + HttpClientRequest.setHeader("host", "b.example.com") + ) + ).pipe( + Effect.flatMap((r) => r.text), + Effect.scoped ) ).toEqual("B") expect( - yield* _( - client( - HttpClientRequest.get("/").pipe( - HttpClientRequest.setHeader("host", "b.org") - ) - ), - HttpClientResponse.text + yield* client.execute( + HttpClientRequest.get("/").pipe( + HttpClientRequest.setHeader("host", "b.org") + ) + ).pipe( + Effect.flatMap((r) => r.text), + Effect.scoped ) ).toEqual("B") expect( - yield* _( - client( - HttpClientRequest.get("/").pipe( - HttpClientRequest.setHeader("host", "c.example.com") - ) - ), - HttpClientResponse.text + yield* client.execute( + HttpClientRequest.get("/").pipe( + HttpClientRequest.setHeader("host", "c.example.com") + ) + ).pipe( + Effect.flatMap((r) => r.text), + Effect.scoped ) ).toEqual("C") }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("html", () => - Effect.gen(function*(_) { - yield* _( - HttpRouter.empty, + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( HttpRouter.get("/home", HttpServerResponse.html("")), HttpRouter.get( "/about", @@ -573,18 +530,17 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const home = yield* _(HttpClientRequest.get("/home"), client, HttpClientResponse.text) + const home = yield* client.get("/home").pipe(Effect.flatMap((r) => r.text), Effect.scoped) expect(home).toEqual("") - const about = yield* _(HttpClientRequest.get("/about"), client, HttpClientResponse.text) + const about = yield* client.get("/about").pipe(Effect.flatMap((r) => r.text), Effect.scoped) expect(about).toEqual("") - const stream = yield* _(HttpClientRequest.get("/stream"), client, HttpClientResponse.text) + const stream = yield* client.get("/stream").pipe(Effect.flatMap((r) => r.text), Effect.scoped) expect(stream).toEqual("123hello") }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scoped("setCookie", () => - Effect.gen(function*(_) { - yield* _( - HttpRouter.empty, + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( HttpRouter.get( "/home", HttpServerResponse.empty().pipe( @@ -604,7 +560,7 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const res = yield* _(HttpClientRequest.get("/home"), client, Effect.scoped) + const res = yield* client.get("/home").pipe(Effect.scoped) assert.deepStrictEqual( res.cookies.toJSON(), Cookies.fromReadonlyRecord({ @@ -624,22 +580,21 @@ describe("HttpServer", () => { }).pipe(Effect.provide(NodeHttpServer.layerTest))) it.scopedLive("uninterruptible routes", () => - Effect.gen(function*(_) { - yield* _( - HttpRouter.empty, + Effect.gen(function*() { + yield* HttpRouter.empty.pipe( HttpRouter.get( "/home", - Effect.gen(function*(_) { + Effect.gen(function*() { const fiber = Option.getOrThrow(Fiber.getCurrentFiber()) setTimeout(() => fiber.unsafeInterruptAsFork(fiber.id()), 10) - return yield* _(HttpServerResponse.empty(), Effect.delay(50)) + return yield* HttpServerResponse.empty().pipe(Effect.delay(50)) }), { uninterruptible: true } ), HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const res = yield* _(HttpClientRequest.get("/home"), client, Effect.scoped) + const res = yield* client.get("/home").pipe(Effect.scoped) assert.strictEqual(res.status, 204) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -648,7 +603,7 @@ describe("HttpServer", () => { Effect.gen(function*() { yield* HttpRouter.empty.pipe(HttpServer.serveEffect()) const client = yield* HttpClient.HttpClient - const res = yield* HttpClientRequest.get("/home").pipe(client, Effect.scoped) + const res = yield* client.get("/").pipe(Effect.scoped) assert.strictEqual(res.status, 404) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -666,7 +621,7 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const res = yield* HttpClientRequest.get("/home").pipe(client) + const res = yield* client.get("/home") assert.strictEqual(res.status, 599) const err = yield* HttpClientResponse.schemaBodyJson(CustomError)(res) assert.deepStrictEqual(err, new CustomError({ name: "test" })) @@ -686,7 +641,10 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const res = yield* HttpClientRequest.get("/user").pipe(client, HttpClientResponse.schemaBodyJsonScoped(User)) + const res = yield* client.get("/user").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(User)), + Effect.scoped + ) assert.deepStrictEqual(res, new User({ name: "test" })) }).pipe(Effect.provide(NodeHttpServer.layerTest))) }) @@ -698,7 +656,7 @@ describe("HttpServer", () => { HttpServer.serveEffect(() => Effect.fail("boom")) ) const client = yield* HttpClient.HttpClient - const res = yield* HttpClientRequest.get("/").pipe(client) + const res = yield* client.get("/") assert.deepStrictEqual(res.status, 500) }).pipe(Effect.provide(NodeHttpServer.layerTest))) diff --git a/packages/platform/README.md b/packages/platform/README.md index 839734b559..60876e03f2 100644 --- a/packages/platform/README.md +++ b/packages/platform/README.md @@ -564,42 +564,46 @@ Effect.gen(function* () { ## Overview -An `HttpClient` is a function that takes a request and produces a certain value `A` in an effectful way (possibly resulting in an error `E` and depending on some requirement `R`). +The `@effect/platform/HttpClient*` modules provide a way to send HTTP requests, +handle responses, and abstract over the differences between platforms. -```ts -type HttpClient = (request: HttpClientRequest): Effect -``` +The `HttpClient` interface has a set of methods for sending requests: -Generally, you'll deal with a specialization called `Default` where `A`, `E`, and `R` are predefined: +- `.execute` - takes a `HttpClientRequest` and returns a `HttpClientResponse` +- `.{get, post, ...}` - convenience methods for creating a request and + executing it in one step -```ts -type Default = (request: HttpClientRequest): Effect -``` - -The goal of `Default` is straightforward: transform a `HttpClientRequest` into a `HttpClientResponse`. +To access the `HttpClient`, you can use the `HttpClient.HttpClient` `Context.Tag`. +This will give you access to a `HttpClient.Service` instance, which is the default +type of the `HttpClient` interface. ### A First Example: Retrieving JSON Data (GET) Here's a simple example demonstrating how to retrieve JSON data using `HttpClient` from `@effect/platform`. ```ts -import { - HttpClient, - HttpClientRequest, - HttpClientResponse -} from "@effect/platform" +import { FetchHttpClient, HttpClient } from "@effect/platform" import { Effect } from "effect" -const req = HttpClientRequest.get( - "https://jsonplaceholder.typicode.com/posts/1" -) +const program = Effect.gen(function* () { + // access the HttpClient + const client = yield* HttpClient.HttpClient -// HttpClient.fetch is a Default -const res = HttpClient.fetch(req) + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/posts/1" + ) -const json = HttpClientResponse.json(res) + const json = yield* response.json -Effect.runPromise(json).then(console.log) + console.log(json) +}).pipe( + // ensure the request is aborted if the program is interrupted + Effect.scoped, + // provide the HttpClient + Effect.provide(FetchHttpClient.layer) +) + +Effect.runPromise(program) /* Output: { @@ -614,29 +618,15 @@ Output: */ ``` -In this example: - -- `HttpClientRequest.get` creates a GET request to the specified URL. -- `HttpClient.fetch` executes the request. -- `HttpClientResponse.json` converts the response to JSON. -- `Effect.runPromise` runs the effect and logs the result. - -### Built-in Defaults +### Custom `HttpClient.Service`'s -| Default | Description | -| -------------------- | ------------------------------------------------------------------------- | -| `HttpClient.fetch` | Execute the request using the global `fetch` function | -| `HttpClient.fetchOk` | Same as `fetch` but ensures only `2xx` responses are treated as successes | - -### Custom Default - -You can create your own `Default` using the `HttpClient.makeDefault` constructor. +You can create your own `HttpClient.Service` using the `HttpClient.makeService` constructor. ```ts import { HttpClient, HttpClientResponse } from "@effect/platform" import { Effect } from "effect" -const myClient = HttpClient.makeDefault((req) => +const myClient = HttpClient.makeService((req) => Effect.succeed( HttpClientResponse.fromWeb( req, @@ -657,27 +647,25 @@ const myClient = HttpClient.makeDefault((req) => ## Tapping ```ts -import { - HttpClient, - HttpClientRequest, - HttpClientResponse -} from "@effect/platform" +import { FetchHttpClient, HttpClient } from "@effect/platform" import { Console, Effect } from "effect" -const req = HttpClientRequest.get( - "https://jsonplaceholder.typicode.com/posts/1" -) +const program = Effect.gen(function* () { + const client = (yield* HttpClient.HttpClient).pipe( + // Log the request before fetching + HttpClient.tapRequest(Console.log) + ) -// Log the request before fetching -const tapFetch: HttpClient.HttpClient.Default = HttpClient.fetch.pipe( - HttpClient.tapRequest(Console.log) -) + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/posts/1" + ) -const res = tapFetch(req) + const json = yield* response.json -const json = HttpClientResponse.json(res) + console.log(json) +}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) -Effect.runPromise(json).then(console.log) +Effect.runPromise(program) /* Output: { @@ -832,20 +820,21 @@ Output: To convert a GET response to JSON: ```ts -import { - HttpClient, - HttpClientRequest, - HttpClientResponse -} from "@effect/platform" +import { FetchHttpClient, HttpClient } from "@effect/platform" import { NodeRuntime } from "@effect/platform-node" import { Console, Effect } from "effect" -const getPostAsJson = HttpClientRequest.get( - "https://jsonplaceholder.typicode.com/posts/1" -).pipe(HttpClient.fetch, HttpClientResponse.json) +const getPostAsJson = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/posts/1" + ) + return yield* response.json +}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) -NodeRuntime.runMain( - getPostAsJson.pipe(Effect.andThen((post) => Console.log(typeof post, post))) +getPostAsJson.pipe( + Effect.andThen((post) => Console.log(typeof post, post)), + NodeRuntime.runMain ) /* Output: @@ -866,20 +855,21 @@ object { To convert a GET response to text: ```ts -import { - HttpClient, - HttpClientRequest, - HttpClientResponse -} from "@effect/platform" +import { FetchHttpClient, HttpClient } from "@effect/platform" import { NodeRuntime } from "@effect/platform-node" import { Console, Effect } from "effect" -const getPostAsText = HttpClientRequest.get( - "https://jsonplaceholder.typicode.com/posts/1" -).pipe(HttpClient.fetch, HttpClientResponse.text) +const getPostAsJson = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/posts/1" + ) + return yield* response.text +}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) -NodeRuntime.runMain( - getPostAsText.pipe(Effect.andThen((post) => Console.log(typeof post, post))) +getPostAsJson.pipe( + Effect.andThen((post) => Console.log(typeof post, post)), + NodeRuntime.runMain ) /* Output: @@ -899,14 +889,14 @@ string { Here are some APIs you can use to convert the response: -| API | Description | -| ---------------------------------- | ------------------------------------- | -| `HttpClientResponse.arrayBuffer` | Convert to `ArrayBuffer` | -| `HttpClientResponse.formData` | Convert to `FormData` | -| `HttpClientResponse.json` | Convert to JSON | -| `HttpClientResponse.stream` | Convert to a `Stream` of `Uint8Array` | -| `HttpClientResponse.text` | Convert to text | -| `HttpClientResponse.urlParamsBody` | Convert to `Http.urlParams.UrlParams` | +| API | Description | +| ------------------------ | ------------------------------------- | +| `response.arrayBuffer` | Convert to `ArrayBuffer` | +| `response.formData` | Convert to `FormData` | +| `response.json` | Convert to JSON | +| `response.stream` | Convert to a `Stream` of `Uint8Array` | +| `response.text` | Convert to text | +| `response.urlParamsBody` | Convert to `UrlParams` | ### Decoding Data with Schemas @@ -914,8 +904,8 @@ A common use case when fetching data is to validate the received format. For thi ```ts import { + FetchHttpClient, HttpClient, - HttpClientRequest, HttpClientResponse } from "@effect/platform" import { NodeRuntime } from "@effect/platform-node" @@ -931,17 +921,17 @@ const Post = Schema.Struct({ const getPostAndValidate: Effect.Effect<{ readonly id: number; readonly title: string; -}, Http.error.HttpClientError | ParseError, never> +}, HttpClientError | ParseError, never> */ -const getPostAndValidate = HttpClientRequest.get( - "https://jsonplaceholder.typicode.com/posts/1" -).pipe( - HttpClient.fetch, - Effect.andThen(HttpClientResponse.schemaBodyJson(Post)), - Effect.scoped -) +const getPostAndValidate = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/posts/1" + ) + return yield* HttpClientResponse.schemaBodyJson(Post)(response) +}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) -NodeRuntime.runMain(getPostAndValidate.pipe(Effect.andThen(Console.log))) +getPostAndValidate.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) /* Output: { @@ -964,19 +954,19 @@ You can use `HttpClient.filterStatusOk`, or `HttpClient.fetchOk` to ensure only In this example, we attempt to fetch a non-existent page and don't receive any error: ```ts -import { - HttpClient, - HttpClientRequest, - HttpClientResponse -} from "@effect/platform" +import { FetchHttpClient, HttpClient } from "@effect/platform" import { NodeRuntime } from "@effect/platform-node" import { Console, Effect } from "effect" -const getText = HttpClientRequest.get( - "https://jsonplaceholder.typicode.com/non-existing-page" -).pipe(HttpClient.fetch, HttpClientResponse.text) +const getText = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/non-existing-page" + ) + return yield* response.text +}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) -NodeRuntime.runMain(getText.pipe(Effect.andThen(Console.log))) +getText.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) /* Output: {} @@ -986,19 +976,19 @@ Output: However, if we use `HttpClient.filterStatusOk`, an error is logged: ```ts -import { - HttpClient, - HttpClientRequest, - HttpClientResponse -} from "@effect/platform" +import { FetchHttpClient, HttpClient } from "@effect/platform" import { NodeRuntime } from "@effect/platform-node" import { Console, Effect } from "effect" -const getText = HttpClientRequest.get( - "https://jsonplaceholder.typicode.com/non-existing-page" -).pipe(HttpClient.filterStatusOk(HttpClient.fetch), HttpClientResponse.text) +const getText = Effect.gen(function* () { + const client = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk) + const response = yield* client.get( + "https://jsonplaceholder.typicode.com/non-existing-page" + ) + return yield* response.text +}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) -NodeRuntime.runMain(getText.pipe(Effect.andThen(Console.log))) +getText.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) /* Output: timestamp=... level=ERROR fiber=#0 cause="ResponseError: StatusCode error (404 GET https://jsonplaceholder.typicode.com/non-existing-page): non 2xx status code @@ -1006,60 +996,36 @@ timestamp=... level=ERROR fiber=#0 cause="ResponseError: StatusCode error (404 G */ ``` -Note that you can use `HttpClient.fetchOk` as a shortcut for `HttpClient.filterStatusOk(HttpClient.fetch)`: - -```ts -const getText = HttpClientRequest.get( - "https://jsonplaceholder.typicode.com/non-existing-page" -).pipe(HttpClient.fetchOk, HttpClientResponse.text) -``` - -You can also create your own status-based filters. In fact, `HttpClient.filterStatusOk` is just a shortcut for the following filter: - -```ts -const getText = HttpClientRequest.get( - "https://jsonplaceholder.typicode.com/non-existing-page" -).pipe( - HttpClient.filterStatus( - HttpClient.fetch, - (status) => status >= 200 && status < 300 - ), - HttpClientResponse.text -) - -/* -Output: -timestamp=... level=ERROR fiber=#0 cause="ResponseError: StatusCode error (404 GET https://jsonplaceholder.typicode.com/non-existing-page): invalid status code - ... stack trace ... -*/ -``` - ## POST -To make a POST request, you can use the `HttpClientRequest.post` function provided by the `HttpClient` module. Here's an example of how to create and send a POST request: +To make a POST request, you can use the `HttpClientRequest.post` function provided by the `HttpClientRequest` module. Here's an example of how to create and send a POST request: ```ts import { + FetchHttpClient, HttpClient, - HttpClientRequest, - HttpClientResponse + HttpClientRequest } from "@effect/platform" import { NodeRuntime } from "@effect/platform-node" import { Console, Effect } from "effect" -const addPost = HttpClientRequest.post( - "https://jsonplaceholder.typicode.com/posts" -).pipe( - HttpClientRequest.jsonBody({ - title: "foo", - body: "bar", - userId: 1 - }), - Effect.andThen(HttpClient.fetch), - HttpClientResponse.json -) +const addPost = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + return yield* HttpClientRequest.post( + "https://jsonplaceholder.typicode.com/posts" + ).pipe( + HttpClientRequest.bodyJson({ + title: "foo", + body: "bar", + userId: 1 + }), + Effect.flatMap(client.execute), + Effect.flatMap((res) => res.json), + Effect.scoped + ) +}).pipe(Effect.provide(FetchHttpClient.layer)) -NodeRuntime.runMain(addPost.pipe(Effect.andThen(Console.log))) +addPost.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) /* Output: { title: 'foo', body: 'bar', userId: 1, id: 101 } @@ -1072,29 +1038,33 @@ In the following example, we send the data as text: ```ts import { + FetchHttpClient, HttpClient, - HttpClientRequest, - HttpClientResponse + HttpClientRequest } from "@effect/platform" import { NodeRuntime } from "@effect/platform-node" import { Console, Effect } from "effect" -const addPost = HttpClientRequest.post( - "https://jsonplaceholder.typicode.com/posts" -).pipe( - HttpClientRequest.textBody( - JSON.stringify({ - title: "foo", - body: "bar", - userId: 1 - }), - "application/json; charset=UTF-8" - ), - HttpClient.fetch, - HttpClientResponse.json -) +const addPost = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + return yield* HttpClientRequest.post( + "https://jsonplaceholder.typicode.com/posts" + ).pipe( + HttpClientRequest.bodyText( + JSON.stringify({ + title: "foo", + body: "bar", + userId: 1 + }), + "application/json; charset=UTF-8" + ), + client.execute, + Effect.flatMap((res) => res.json), + Effect.scoped + ) +}).pipe(Effect.provide(FetchHttpClient.layer)) -NodeRuntime.runMain(Effect.andThen(addPost, Console.log)) +addPost.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) /* Output: { title: 'foo', body: 'bar', userId: 1, id: 101 } @@ -1107,6 +1077,7 @@ A common use case when fetching data is to validate the received format. For thi ```ts import { + FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse @@ -1120,20 +1091,26 @@ const Post = Schema.Struct({ title: Schema.String }) -const addPost = HttpClientRequest.post( - "https://jsonplaceholder.typicode.com/posts" -).pipe( - HttpClientRequest.jsonBody({ - title: "foo", - body: "bar", - userId: 1 - }), - Effect.andThen(HttpClient.fetch), - Effect.andThen(HttpClientResponse.schemaBodyJson(Post)), - Effect.scoped -) +const addPost = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + return yield* HttpClientRequest.post( + "https://jsonplaceholder.typicode.com/posts" + ).pipe( + HttpClientRequest.bodyText( + JSON.stringify({ + title: "foo", + body: "bar", + userId: 1 + }), + "application/json; charset=UTF-8" + ), + client.execute, + Effect.flatMap(HttpClientResponse.schemaBodyJson(Post)), + Effect.scoped + ) +}).pipe(Effect.provide(FetchHttpClient.layer)) -NodeRuntime.runMain(addPost.pipe(Effect.andThen(Console.log))) +addPost.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) /* Output: { id: 101, title: 'foo' } @@ -1147,30 +1124,31 @@ Output: To test HTTP requests, you can inject a mock fetch implementation. ```ts -import { - HttpClient, - HttpClientRequest, - HttpClientResponse -} from "@effect/platform" +import { FetchHttpClient, HttpClient } from "@effect/platform" import { Effect, Layer } from "effect" import * as assert from "node:assert" // Mock fetch implementation -const FetchTest = Layer.succeed(HttpClient.Fetch, () => +const FetchTest = Layer.succeed(FetchHttpClient.Fetch, () => Promise.resolve(new Response("not found", { status: 404 })) ) -// Program to test -const program = HttpClientRequest.get("https://www.google.com/").pipe( - HttpClient.fetch, - HttpClientResponse.text -) +const TestLayer = FetchHttpClient.layer.pipe(Layer.provide(FetchTest)) + +const program = Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + + return yield* client.get("https://www.google.com/").pipe( + Effect.flatMap((res) => res.text), + Effect.scoped + ) +}) // Test Effect.gen(function* () { const response = yield* program assert.equal(response, "not found") -}).pipe(Effect.provide(FetchTest), Effect.runPromise) +}).pipe(Effect.provide(TestLayer), Effect.runPromise) ``` # HTTP Server diff --git a/packages/platform/src/FetchHttpClient.ts b/packages/platform/src/FetchHttpClient.ts new file mode 100644 index 0000000000..94ad39bcf8 --- /dev/null +++ b/packages/platform/src/FetchHttpClient.ts @@ -0,0 +1,25 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import type * as Layer from "effect/Layer" +import type { HttpClient } from "./HttpClient.js" +import * as internal from "./internal/fetchHttpClient.js" + +/** + * @since 1.0.0 + * @category tags + */ +export class Fetch extends Context.Tag(internal.fetchTagKey)() {} + +/** + * @since 1.0.0 + * @category tags + */ +export class RequestInit extends Context.Tag(internal.requestInitTagKey)() {} + +/** + * @since 1.0.0 + * @category layers + */ +export const layer: Layer.Layer = internal.layer diff --git a/packages/platform/src/HttpApiClient.ts b/packages/platform/src/HttpApiClient.ts index 74034d5bb4..a31c9cb896 100644 --- a/packages/platform/src/HttpApiClient.ts +++ b/packages/platform/src/HttpApiClient.ts @@ -56,10 +56,10 @@ export type Client = [A] extends export const make = ( api: A, options?: { - readonly transformClient?: ((client: HttpClient.HttpClient.Default) => HttpClient.HttpClient.Default) | undefined + readonly transformClient?: ((client: HttpClient.HttpClient.Service) => HttpClient.HttpClient.Service) | undefined readonly baseUrl?: string | undefined } -): Effect.Effect>, never, HttpApi.HttpApi.Context | HttpClient.HttpClient.Default> => +): Effect.Effect>, never, HttpApi.HttpApi.Context | HttpClient.HttpClient.Service> => Effect.gen(function*() { const context = yield* Effect.context() const httpClient = (yield* HttpClient.HttpClient).pipe( @@ -165,13 +165,13 @@ export const make = ( const baseRequest = HttpClientRequest.make(endpoint.method)(url) return (isMultipart ? Effect.succeed(baseRequest.pipe( - HttpClientRequest.formDataBody(request.payload) + HttpClientRequest.bodyFormData(request.payload) )) : encodePayload._tag === "Some" ? encodePayload.value(request.payload).pipe( Effect.flatMap((payload) => HttpMethod.hasBody(endpoint.method) - ? HttpClientRequest.jsonBody(baseRequest, payload) + ? HttpClientRequest.bodyJson(baseRequest, payload) : Effect.succeed(HttpClientRequest.setUrlParams(baseRequest, payload as any)) ), Effect.orDie @@ -186,7 +186,7 @@ export const make = ( ) : identity, Effect.flatMap((request) => - Effect.flatMap(httpClient(request), (response) => + Effect.flatMap(httpClient.execute(request), (response) => response.status !== successStatus ? handleError(request, response) : Effect.succeed(response)) diff --git a/packages/platform/src/HttpClient.ts b/packages/platform/src/HttpClient.ts index 1240abcb73..45f8b3d819 100644 --- a/packages/platform/src/HttpClient.ts +++ b/packages/platform/src/HttpClient.ts @@ -9,7 +9,7 @@ import type * as Effect from "effect/Effect" import type { RuntimeFiber } from "effect/Fiber" import type * as FiberRef from "effect/FiberRef" import type { Inspectable } from "effect/Inspectable" -import type * as Layer from "effect/Layer" +import type { Layer } from "effect/Layer" import type { Pipeable } from "effect/Pipeable" import type * as Predicate from "effect/Predicate" import type { Ref } from "effect/Ref" @@ -38,10 +38,16 @@ export type TypeId = typeof TypeId * @category models */ export interface HttpClient extends Pipeable, Inspectable { - (request: ClientRequest.HttpClientRequest): Effect.Effect readonly [TypeId]: TypeId - readonly preprocess: HttpClient.Preprocess - readonly execute: HttpClient.Execute + readonly execute: (request: ClientRequest.HttpClientRequest) => Effect.Effect + + readonly get: (url: string | URL, options?: ClientRequest.Options.NoBody) => Effect.Effect + readonly head: (url: string | URL, options?: ClientRequest.Options.NoBody) => Effect.Effect + readonly post: (url: string | URL, options?: ClientRequest.Options.NoUrl) => Effect.Effect + readonly patch: (url: string | URL, options?: ClientRequest.Options.NoUrl) => Effect.Effect + readonly put: (url: string | URL, options?: ClientRequest.Options.NoUrl) => Effect.Effect + readonly del: (url: string | URL, options?: ClientRequest.Options.NoUrl) => Effect.Effect + readonly options: (url: string | URL, options?: ClientRequest.Options.NoUrl) => Effect.Effect } /** @@ -60,7 +66,7 @@ export declare namespace HttpClient { * @since 1.0.0 * @category models */ - export type Execute = ( + export type Postprocess = ( request: Effect.Effect ) => Effect.Effect @@ -74,46 +80,14 @@ export declare namespace HttpClient { * @since 1.0.0 * @category models */ - export type Default = WithResponse + export type Service = WithResponse } -/** - * @since 1.0.0 - * @category models - */ -export interface Fetch { - readonly _: unique symbol -} - -/** - * @since 1.0.0 - * @category tags - */ -export const HttpClient: Context.Tag = internal.tag - /** * @since 1.0.0 * @category tags */ -export const Fetch: Context.Tag = internal.Fetch - -/** - * @since 1.0.0 - * @category layers - */ -export const layer: Layer.Layer = internal.layer - -/** - * @since 1.0.0 - * @category constructors - */ -export const fetch: HttpClient.Default = internal.fetch - -/** - * @since 1.0.0 - * @category constructors - */ -export const fetchOk: HttpClient.Default = internal.fetchOk +export const HttpClient: Context.Tag = internal.tag /** * @since 1.0.0 @@ -262,14 +236,14 @@ export const make: ( * @since 1.0.0 * @category constructors */ -export const makeDefault: ( +export const makeService: ( f: ( request: ClientRequest.HttpClientRequest, url: URL, signal: AbortSignal, fiber: RuntimeFiber ) => Effect.Effect -) => HttpClient.Default = internal.makeDefault +) => HttpClient.Service = internal.makeService /** * @since 1.0.0 @@ -319,20 +293,6 @@ export const mapEffect: { (self: HttpClient, f: (a: A) => Effect.Effect): HttpClient } = internal.mapEffect -/** - * @since 1.0.0 - * @category mapping & sequencing - */ -export const mapEffectScoped: { - ( - f: (a: A) => Effect.Effect - ): (self: HttpClient) => HttpClient | Exclude> - ( - self: HttpClient, - f: (a: A) => Effect.Effect - ): HttpClient | Exclude> -} = internal.mapEffectScoped - /** * @since 1.0.0 * @category mapping & sequencing @@ -365,7 +325,7 @@ export const mapRequestEffect: { * @since 1.0.0 * @category mapping & sequencing */ -export const mapInputRequest: { +export const mapRequestInput: { ( f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest ): (self: HttpClient) => HttpClient @@ -373,13 +333,13 @@ export const mapInputRequest: { self: HttpClient, f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest ): HttpClient -} = internal.mapInputRequest +} = internal.mapRequestInput /** * @since 1.0.0 * @category mapping & sequencing */ -export const mapInputRequestEffect: { +export const mapRequestInputEffect: { ( f: (a: ClientRequest.HttpClientRequest) => Effect.Effect ): (self: HttpClient) => HttpClient @@ -387,20 +347,42 @@ export const mapInputRequestEffect: { self: HttpClient, f: (a: ClientRequest.HttpClientRequest) => Effect.Effect ): HttpClient -} = internal.mapInputRequestEffect +} = internal.mapRequestInputEffect + +/** + * @since 1.0.0 + * @category error handling + */ +export declare namespace Retry { + /** + * @since 1.0.0 + * @category error handling + */ + export type Return> = HttpClient< + A, + | (O extends { schedule: Schedule.Schedule } ? E + : O extends { until: Predicate.Refinement } ? E2 + : E) + | (O extends { while: (...args: Array) => Effect.Effect } ? E : never) + | (O extends { until: (...args: Array) => Effect.Effect } ? E : never), + | R + | (O extends { schedule: Schedule.Schedule } ? R : never) + | (O extends { while: (...args: Array) => Effect.Effect } ? R : never) + | (O extends { until: (...args: Array) => Effect.Effect } ? R : never) + > extends infer Z ? Z : never +} /** * @since 1.0.0 * @category error handling */ export const retry: { - ( - policy: Schedule.Schedule + >(options: O): (self: HttpClient) => Retry.Return + ( + policy: Schedule.Schedule, R1> ): (self: HttpClient) => HttpClient - ( - self: HttpClient, - policy: Schedule.Schedule - ): HttpClient + >(self: HttpClient, options: O): Retry.Return + (self: HttpClient, policy: Schedule.Schedule): HttpClient } = internal.retry /** @@ -512,15 +494,7 @@ export const withTracerPropagation: { /** * @since 1.0.0 - * @category fiber refs - */ -export const currentFetchOptions: FiberRef.FiberRef = internal.currentFetchOptions - -/** - * @since 1.0.0 - * @category fiber refs */ -export const withFetchOptions: { - (options: RequestInit): (effect: Effect.Effect) => Effect.Effect - (effect: Effect.Effect, options: RequestInit): Effect.Effect -} = internal.withFetchOptions +export const layerMergedContext: ( + effect: Effect.Effect +) => Layer = internal.layerMergedContext diff --git a/packages/platform/src/HttpClientRequest.ts b/packages/platform/src/HttpClientRequest.ts index e51eddb2c7..39f6485d72 100644 --- a/packages/platform/src/HttpClientRequest.ts +++ b/packages/platform/src/HttpClientRequest.ts @@ -36,7 +36,7 @@ export type TypeId = typeof TypeId * @category models */ export interface HttpClientRequest - extends Effect.Effect, Inspectable + extends Effect.Effect, Inspectable { readonly [TypeId]: TypeId readonly method: HttpMethod @@ -301,73 +301,73 @@ export const setBody: { * @since 1.0.0 * @category combinators */ -export const uint8ArrayBody: { +export const bodyUint8Array: { (body: Uint8Array, contentType?: string): (self: HttpClientRequest) => HttpClientRequest (self: HttpClientRequest, body: Uint8Array, contentType?: string): HttpClientRequest -} = internal.uint8ArrayBody +} = internal.bodyUint8Array /** * @since 1.0.0 * @category combinators */ -export const textBody: { +export const bodyText: { (body: string, contentType?: string): (self: HttpClientRequest) => HttpClientRequest (self: HttpClientRequest, body: string, contentType?: string): HttpClientRequest -} = internal.textBody +} = internal.bodyText /** * @since 1.0.0 * @category combinators */ -export const jsonBody: { +export const bodyJson: { (body: unknown): (self: HttpClientRequest) => Effect.Effect (self: HttpClientRequest, body: unknown): Effect.Effect -} = internal.jsonBody +} = internal.bodyJson /** * @since 1.0.0 * @category combinators */ -export const unsafeJsonBody: { +export const bodyUnsafeJson: { (body: unknown): (self: HttpClientRequest) => HttpClientRequest (self: HttpClientRequest, body: unknown): HttpClientRequest -} = internal.unsafeJsonBody +} = internal.bodyUnsafeJson /** * @since 1.0.0 * @category combinators */ -export const schemaBody: ( +export const schemaBodyJson: ( schema: Schema.Schema, options?: ParseOptions | undefined ) => { (body: A): (self: HttpClientRequest) => Effect.Effect (self: HttpClientRequest, body: A): Effect.Effect -} = internal.schemaBody +} = internal.schemaBodyJson /** * @since 1.0.0 * @category combinators */ -export const urlParamsBody: { +export const bodyUrlParams: { (input: UrlParams.Input): (self: HttpClientRequest) => HttpClientRequest (self: HttpClientRequest, input: UrlParams.Input): HttpClientRequest -} = internal.urlParamsBody +} = internal.bodyUrlParams /** * @since 1.0.0 * @category combinators */ -export const formDataBody: { +export const bodyFormData: { (body: FormData): (self: HttpClientRequest) => HttpClientRequest (self: HttpClientRequest, body: FormData): HttpClientRequest -} = internal.formDataBody +} = internal.bodyFormData /** * @since 1.0.0 * @category combinators */ -export const streamBody: { +export const bodyStream: { ( body: Stream.Stream, options?: { readonly contentType?: string | undefined; readonly contentLength?: number | undefined } | undefined @@ -377,13 +377,13 @@ export const streamBody: { body: Stream.Stream, options?: { readonly contentType?: string | undefined; readonly contentLength?: number | undefined } | undefined ): HttpClientRequest -} = internal.streamBody +} = internal.bodyStream /** * @since 1.0.0 * @category combinators */ -export const fileBody: { +export const bodyFile: { ( path: string, options?: FileSystem.StreamOptions & { readonly contentType?: string } @@ -393,13 +393,13 @@ export const fileBody: { path: string, options?: FileSystem.StreamOptions & { readonly contentType?: string } ): Effect.Effect -} = internal.fileBody +} = internal.bodyFile /** * @since 1.0.0 * @category combinators */ -export const fileWebBody: { +export const bodyFileWeb: { (file: Body.HttpBody.FileLike): (self: HttpClientRequest) => HttpClientRequest (self: HttpClientRequest, file: Body.HttpBody.FileLike): HttpClientRequest -} = internal.fileWebBody +} = internal.bodyFileWeb diff --git a/packages/platform/src/HttpClientResponse.ts b/packages/platform/src/HttpClientResponse.ts index 9b17daaa2c..5e44cf975b 100644 --- a/packages/platform/src/HttpClientResponse.ts +++ b/packages/platform/src/HttpClientResponse.ts @@ -7,12 +7,12 @@ import type * as Schema from "@effect/schema/Schema" import type * as Effect from "effect/Effect" import type * as Scope from "effect/Scope" import type * as Stream from "effect/Stream" +import type { Unify } from "effect/Unify" import type * as Cookies from "./Cookies.js" import type * as Error from "./HttpClientError.js" import type * as ClientRequest from "./HttpClientRequest.js" import type * as IncomingMessage from "./HttpIncomingMessage.js" import * as internal from "./internal/httpClientResponse.js" -import type * as UrlParams from "./UrlParams.js" export { /** @@ -20,11 +20,6 @@ export { * @category schema */ schemaBodyJson, - /** - * @since 1.0.0 - * @category schema - */ - schemaBodyJsonScoped, /** * @since 1.0.0 * @category schema @@ -34,17 +29,7 @@ export { * @since 1.0.0 * @category schema */ - schemaBodyUrlParamsScoped, - /** - * @since 1.0.0 - * @category schema - */ - schemaHeaders, - /** - * @since 1.0.0 - * @category schema - */ - schemaHeadersScoped + schemaHeaders } from "./HttpIncomingMessage.js" /** @@ -111,41 +96,6 @@ export const schemaNoBody: < options?: ParseOptions | undefined ) => (self: HttpClientResponse) => Effect.Effect = internal.schemaNoBody -/** - * @since 1.0.0 - * @category accessors - */ -export const arrayBuffer: ( - effect: Effect.Effect -) => Effect.Effect> = internal.arrayBuffer - -/** - * @since 1.0.0 - * @category accessors - */ -export const formData: ( - effect: Effect.Effect -) => Effect.Effect> = internal.formData - -/** - * @since 1.0.0 - * @category accessors - */ -export const json: ( - effect: Effect.Effect -) => Effect.Effect> = internal.json - -const void_: ( - effect: Effect.Effect -) => Effect.Effect> = internal.void_ -export { - /** - * @since 1.0.0 - * @category accessors - */ - void_ as void -} - /** * @since 1.0.0 * @category accessors @@ -154,64 +104,6 @@ export const stream: ( effect: Effect.Effect ) => Stream.Stream> = internal.stream -/** - * @since 1.0.0 - * @category accessors - */ -export const text: ( - effect: Effect.Effect -) => Effect.Effect> = internal.text - -/** - * @since 1.0.0 - * @category accessors - */ -export const urlParamsBody: ( - effect: Effect.Effect -) => Effect.Effect> = internal.urlParamsBody - -/** - * @since 1.0.0 - * @category schema - */ -export const schemaJsonScoped: < - R, - I extends { - readonly status?: number | undefined - readonly headers?: Readonly> | undefined - readonly body?: unknown - }, - A ->( - schema: Schema.Schema, - options?: ParseOptions | undefined -) => ( - effect: Effect.Effect -) => Effect.Effect< - A, - E | Error.ResponseError | ParseResult.ParseError, - Exclude | Exclude -> = internal.schemaJsonScoped - -/** - * @since 1.0.0 - * @category schema - */ -export const schemaNoBodyScoped: < - R, - I extends { - readonly status?: number | undefined - readonly headers?: Readonly> | undefined - }, - A ->( - schema: Schema.Schema, - options?: ParseOptions | undefined -) => ( - effect: Effect.Effect -) => Effect.Effect | Exclude> = - internal.schemaNoBodyScoped - /** * @since 1.0.0 * @category pattern matching @@ -226,7 +118,7 @@ export const matchStatus: { readonly "5xx"?: (_: HttpClientResponse) => any readonly orElse: (_: HttpClientResponse) => any } - >(cases: Cases): (self: HttpClientResponse) => Cases[keyof Cases] extends (_: any) => infer R ? R : never + >(cases: Cases): (self: HttpClientResponse) => Cases[keyof Cases] extends (_: any) => infer R ? Unify : never < const Cases extends { readonly [status: number]: (_: HttpClientResponse) => any @@ -236,55 +128,5 @@ export const matchStatus: { readonly "5xx"?: (_: HttpClientResponse) => any readonly orElse: (_: HttpClientResponse) => any } - >(self: HttpClientResponse, cases: Cases): Cases[keyof Cases] extends (_: any) => infer R ? R : never + >(self: HttpClientResponse, cases: Cases): Cases[keyof Cases] extends (_: any) => infer R ? Unify : never } = internal.matchStatus - -/** - * @since 1.0.0 - * @category pattern matching - */ -export const matchStatusScoped: { - < - const Cases extends { - readonly [status: number]: (_: HttpClientResponse) => Effect.Effect - readonly "2xx"?: (_: HttpClientResponse) => Effect.Effect - readonly "3xx"?: (_: HttpClientResponse) => Effect.Effect - readonly "4xx"?: (_: HttpClientResponse) => Effect.Effect - readonly "5xx"?: (_: HttpClientResponse) => Effect.Effect - readonly orElse: (_: HttpClientResponse) => Effect.Effect - } - >( - cases: Cases - ): ( - self: Effect.Effect - ) => Effect.Effect< - Cases[keyof Cases] extends (_: any) => Effect.Effect ? _A : never, - E | (Cases[keyof Cases] extends (_: any) => Effect.Effect ? _E : never), - Exclude< - R | (Cases[keyof Cases] extends (_: any) => Effect.Effect ? _R : never), - Scope.Scope - > - > - < - E, - R, - const Cases extends { - readonly [status: number]: (_: HttpClientResponse) => Effect.Effect - readonly "2xx"?: (_: HttpClientResponse) => Effect.Effect - readonly "3xx"?: (_: HttpClientResponse) => Effect.Effect - readonly "4xx"?: (_: HttpClientResponse) => Effect.Effect - readonly "5xx"?: (_: HttpClientResponse) => Effect.Effect - readonly orElse: (_: HttpClientResponse) => Effect.Effect - } - >( - self: Effect.Effect, - cases: Cases - ): Effect.Effect< - Cases[keyof Cases] extends (_: any) => Effect.Effect ? _A : never, - E | (Cases[keyof Cases] extends (_: any) => Effect.Effect ? _E : never), - Exclude< - R | (Cases[keyof Cases] extends (_: any) => Effect.Effect ? _R : never), - Scope.Scope - > - > -} = internal.matchStatusScoped diff --git a/packages/platform/src/HttpIncomingMessage.ts b/packages/platform/src/HttpIncomingMessage.ts index 29f616c5b5..e217a94ebc 100644 --- a/packages/platform/src/HttpIncomingMessage.ts +++ b/packages/platform/src/HttpIncomingMessage.ts @@ -10,7 +10,6 @@ import { dual } from "effect/Function" import * as Global from "effect/GlobalValue" import type { Inspectable } from "effect/Inspectable" import * as Option from "effect/Option" -import type * as Scope from "effect/Scope" import type * as Stream from "effect/Stream" import * as FileSystem from "./FileSystem.js" import type * as Headers from "./Headers.js" @@ -53,18 +52,6 @@ export const schemaBodyJson = (schema: Schema.Schema, options? Effect.flatMap(self.json, parse) } -/** - * @since 1.0.0 - * @category schema - */ -export const schemaBodyJsonScoped = (schema: Schema.Schema, options?: ParseOptions | undefined) => { - const decode = schemaBodyJson(schema, options) - return ( - effect: Effect.Effect, E2, R2> - ): Effect.Effect | Exclude> => - Effect.scoped(Effect.flatMap(effect, decode)) -} - /** * @since 1.0.0 * @category schema @@ -78,21 +65,6 @@ export const schemaBodyUrlParams = parse(Object.fromEntries(_))) } -/** - * @since 1.0.0 - * @category schema - */ -export const schemaBodyUrlParamsScoped = >, R>( - schema: Schema.Schema, - options?: ParseOptions | undefined -) => { - const decode = schemaBodyUrlParams(schema, options) - return ( - effect: Effect.Effect, E2, R2> - ): Effect.Effect | Exclude> => - Effect.scoped(Effect.flatMap(effect, decode)) -} - /** * @since 1.0.0 * @category schema @@ -105,21 +77,6 @@ export const schemaHeaders = (self: HttpIncomingMessage): Effect.Effect => parse(self.headers) } -/** - * @since 1.0.0 - * @category schema - */ -export const schemaHeadersScoped = >, R>( - schema: Schema.Schema, - options?: ParseOptions | undefined -) => { - const decode = schemaHeaders(schema, options) - return ( - effect: Effect.Effect, E2, R2> - ): Effect.Effect | Exclude> => - Effect.scoped(Effect.flatMap(effect, decode)) -} - /** * @since 1.0.0 * @category fiber refs diff --git a/packages/platform/src/HttpServer.ts b/packages/platform/src/HttpServer.ts index 9a5e155d5e..c972e2a5d8 100644 --- a/packages/platform/src/HttpServer.ts +++ b/packages/platform/src/HttpServer.ts @@ -201,5 +201,5 @@ export const withLogAddress: (layer: Layer.Layer) => Layer.Lay * @since 1.0.0 * @category layers */ -export const layerTestClient: Layer.Layer = +export const layerTestClient: Layer.Layer = internal.layerTestClient diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts index 7764a015ed..b85998a25d 100644 --- a/packages/platform/src/index.ts +++ b/packages/platform/src/index.ts @@ -28,6 +28,11 @@ export * as Error from "./Error.js" */ export * as Etag from "./Etag.js" +/** + * @since 1.0.0 + */ +export * as FetchHttpClient from "./FetchHttpClient.js" + /** * @since 1.0.0 */ diff --git a/packages/platform/src/internal/fetchHttpClient.ts b/packages/platform/src/internal/fetchHttpClient.ts new file mode 100644 index 0000000000..c73ce42ce5 --- /dev/null +++ b/packages/platform/src/internal/fetchHttpClient.ts @@ -0,0 +1,56 @@ +import * as Effect from "effect/Effect" +import * as FiberRef from "effect/FiberRef" +import * as Stream from "effect/Stream" +import type * as Client from "../HttpClient.js" +import * as Error from "../HttpClientError.js" +import * as Method from "../HttpMethod.js" +import * as client from "./httpClient.js" +import * as internalResponse from "./httpClientResponse.js" + +/** @internal */ +export const fetchTagKey = "@effect/platform/FetchHttpClient/Fetch" +/** @internal */ +export const requestInitTagKey = "@effect/platform/FetchHttpClient/FetchOptions" + +const fetch: Client.HttpClient.Service = client.makeService((request, url, signal, fiber) => { + const context = fiber.getFiberRef(FiberRef.currentContext) + const fetch: typeof globalThis.fetch = context.unsafeMap.get(fetchTagKey) ?? globalThis.fetch + const options: RequestInit = context.unsafeMap.get(requestInitTagKey) ?? {} + const headers = new globalThis.Headers(request.headers) + const send = (body: BodyInit | undefined) => + Effect.map( + Effect.tryPromise({ + try: () => + fetch(url, { + ...options, + method: request.method, + headers, + body, + duplex: request.body._tag === "Stream" ? "half" : undefined, + signal + } as any), + catch: (cause) => + new Error.RequestError({ + request, + reason: "Transport", + cause + }) + }), + (response) => internalResponse.fromWeb(request, response) + ) + if (Method.hasBody(request.method)) { + 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) +}) + +/** @internal */ +export const layer = client.layerMergedContext(Effect.succeed(fetch)) diff --git a/packages/platform/src/internal/httpClient.ts b/packages/platform/src/internal/httpClient.ts index 7cee624e0e..8fc588308f 100644 --- a/packages/platform/src/internal/httpClient.ts +++ b/packages/platform/src/internal/httpClient.ts @@ -7,25 +7,23 @@ import type * as Fiber from "effect/Fiber" import * as FiberRef from "effect/FiberRef" import { constFalse, dual } from "effect/Function" import { globalValue } from "effect/GlobalValue" +import * as Inspectable from "effect/Inspectable" import * as Layer from "effect/Layer" import { pipeArguments } from "effect/Pipeable" import * as Predicate from "effect/Predicate" 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 * as Cookies from "../Cookies.js" import * as Headers from "../Headers.js" import type * as Client from "../HttpClient.js" import * as Error from "../HttpClientError.js" import type * as ClientRequest from "../HttpClientRequest.js" import type * as ClientResponse from "../HttpClientResponse.js" -import * as Method from "../HttpMethod.js" import * as TraceContext from "../HttpTraceContext.js" import * as UrlParams from "../UrlParams.js" import * as internalBody from "./httpBody.js" import * as internalRequest from "./httpClientRequest.js" -import * as internalResponse from "./httpClientResponse.js" /** @internal */ export const TypeId: Client.TypeId = Symbol.for( @@ -33,7 +31,7 @@ export const TypeId: Client.TypeId = Symbol.for( ) as Client.TypeId /** @internal */ -export const tag = Context.GenericTag("@effect/platform/HttpClient") +export const tag = Context.GenericTag("@effect/platform/HttpClient") /** @internal */ export const currentTracerDisabledWhen = globalValue( @@ -69,57 +67,72 @@ export const withTracerPropagation = dual< ) => Effect.Effect >(2, (self, enabled) => Effect.locally(self, currentTracerPropagation, enabled)) -/** @internal */ -export const currentFetchOptions = globalValue( - Symbol.for("@effect/platform/HttpClient/currentFetchOptions"), - () => FiberRef.unsafeMake({}) -) - -/** @internal */ -export const withFetchOptions = dual< - ( - options: RequestInit - ) => (effect: Effect.Effect) => Effect.Effect, - ( - effect: Effect.Effect, - options: RequestInit - ) => Effect.Effect ->(2, (self, options) => Effect.locally(self, currentFetchOptions, options)) - -const clientProto = { +const ClientProto = { [TypeId]: TypeId, pipe() { return pipeArguments(this, arguments) + }, + ...Inspectable.BaseProto, + toJSON() { + return { + _id: "@effect/platform/HttpClient" + } + }, + get(this: Client.HttpClient.Service, url: string | URL, options?: ClientRequest.Options.NoBody) { + return this.execute(internalRequest.get(url, options)) + }, + head(this: Client.HttpClient.Service, url: string | URL, options?: ClientRequest.Options.NoBody) { + return this.execute(internalRequest.head(url, options)) + }, + post(this: Client.HttpClient.Service, url: string | URL, options: ClientRequest.Options.NoUrl) { + return this.execute(internalRequest.post(url, options)) + }, + put(this: Client.HttpClient.Service, url: string | URL, options: ClientRequest.Options.NoUrl) { + return this.execute(internalRequest.put(url, options)) + }, + patch(this: Client.HttpClient.Service, url: string | URL, options: ClientRequest.Options.NoUrl) { + return this.execute(internalRequest.patch(url, options)) + }, + del(this: Client.HttpClient.Service, url: string | URL, options?: ClientRequest.Options.NoUrl) { + return this.execute(internalRequest.del(url, options)) + }, + options(this: Client.HttpClient.Service, url: string | URL, options?: ClientRequest.Options.NoBody) { + return this.execute(internalRequest.options(url, options)) } } const isClient = (u: unknown): u is Client.HttpClient => Predicate.hasProperty(u, TypeId) +interface HttpClientImpl extends Client.HttpClient { + readonly preprocess: Client.HttpClient.Preprocess + readonly postprocess: Client.HttpClient.Postprocess +} + /** @internal */ export const make = ( - execute: ( + postprocess: ( request: Effect.Effect ) => Effect.Effect, preprocess: Client.HttpClient.Preprocess ): Client.HttpClient => { - function client(request: ClientRequest.HttpClientRequest) { - return execute(preprocess(request)) + const self = Object.create(ClientProto) + self.preprocess = preprocess + self.postprocess = postprocess + self.execute = function(request: ClientRequest.HttpClientRequest) { + return postprocess(preprocess(request)) } - Object.setPrototypeOf(client, clientProto) - ;(client as any).preprocess = preprocess - ;(client as any).execute = execute - return client as any + return self } /** @internal */ -export const makeDefault = ( +export const makeService = ( f: ( request: ClientRequest.HttpClientRequest, url: URL, signal: AbortSignal, fiber: Fiber.RuntimeFiber ) => Effect.Effect -): Client.HttpClient.Default => +): Client.HttpClient.Service => make((effect) => Effect.flatMap(effect, (request) => Effect.withFiberRuntime((fiber) => { @@ -188,52 +201,6 @@ export const makeDefault = ( ) })), Effect.succeed as Client.HttpClient.Preprocess) -/** @internal */ -export const Fetch = Context.GenericTag( - "@effect/platform/HttpClient/Fetch" -) - -/** @internal */ -export const fetch: Client.HttpClient.Default = makeDefault((request, url, signal, fiber) => { - const context = fiber.getFiberRef(FiberRef.currentContext) - const fetch: typeof globalThis.fetch = context.unsafeMap.get(Fetch.key) ?? globalThis.fetch - const options = fiber.getFiberRef(currentFetchOptions) - const headers = new globalThis.Headers(request.headers) - const send = (body: BodyInit | undefined) => - Effect.map( - Effect.tryPromise({ - try: () => - fetch(url, { - ...options, - method: request.method, - headers, - body, - duplex: request.body._tag === "Stream" ? "half" : undefined, - signal - } as any), - catch: (cause) => - new Error.RequestError({ - request, - reason: "Transport", - cause - }) - }), - (response) => internalResponse.fromWeb(request, response) - ) - if (Method.hasBody(request.method)) { - 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) -}) - /** @internal */ export const transform = dual< ( @@ -249,11 +216,13 @@ export const transform = dual< request: ClientRequest.HttpClientRequest ) => Effect.Effect ) => Client.HttpClient ->(2, (self, f) => - make( - Effect.flatMap((request) => f(self.execute(Effect.succeed(request)), request)), - self.preprocess - )) +>(2, (self, f) => { + const client = self as HttpClientImpl + return make( + Effect.flatMap((request) => f(client.postprocess(Effect.succeed(request)), request)), + client.preprocess + ) +}) /** @internal */ export const filterStatus = dual< @@ -297,12 +266,6 @@ export const filterStatusOk = ( }) )) -/** @internal */ -export const fetchOk: Client.HttpClient.Default = filterStatusOk(fetch) - -/** @internal */ -export const layer = Layer.succeed(tag, fetch) - /** @internal */ export const transformResponse = dual< ( @@ -312,7 +275,10 @@ export const transformResponse = dual< self: Client.HttpClient, f: (effect: Effect.Effect) => Effect.Effect ) => Client.HttpClient ->(2, (self, f) => make((request) => f(self.execute(request)), self.preprocess)) +>(2, (self, f) => { + const client = self as HttpClientImpl + return make((request) => f(client.postprocess(request)), client.preprocess) +}) /** @internal */ export const catchTag: { @@ -584,7 +550,10 @@ export const mapRequest = dual< self: Client.HttpClient, f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest ) => Client.HttpClient ->(2, (self, f) => make(self.execute, (request) => Effect.map(self.preprocess(request), f))) +>(2, (self, f) => { + const client = self as HttpClientImpl + return make(client.postprocess, (request) => Effect.map(client.preprocess(request), f)) +}) /** @internal */ export const mapRequestEffect = dual< @@ -601,10 +570,13 @@ export const mapRequestEffect = dual< a: ClientRequest.HttpClientRequest ) => Effect.Effect ) => Client.HttpClient ->(2, (self, f) => make(self.execute as any, (request) => Effect.flatMap(self.preprocess(request), f))) +>(2, (self, f) => { + const client = self as HttpClientImpl + return make(client.postprocess as any, (request) => Effect.flatMap(client.preprocess(request), f)) +}) /** @internal */ -export const mapInputRequest = dual< +export const mapRequestInput = dual< ( f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest ) => (self: Client.HttpClient) => Client.HttpClient, @@ -612,10 +584,13 @@ export const mapInputRequest = dual< self: Client.HttpClient, f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest ) => Client.HttpClient ->(2, (self, f) => make(self.execute, (request) => self.preprocess(f(request)))) +>(2, (self, f) => { + const client = self as HttpClientImpl + return make(client.postprocess, (request) => client.preprocess(f(request))) +}) /** @internal */ -export const mapInputRequestEffect = dual< +export const mapRequestInputEffect = dual< ( f: ( a: ClientRequest.HttpClientRequest @@ -629,17 +604,29 @@ export const mapInputRequestEffect = dual< a: ClientRequest.HttpClientRequest ) => Effect.Effect ) => Client.HttpClient ->(2, (self, f) => make(self.execute as any, (request) => Effect.flatMap(f(request), self.preprocess))) +>(2, (self, f) => { + const client = self as HttpClientImpl + return make(client.postprocess as any, (request) => Effect.flatMap(f(request), client.preprocess)) +}) /** @internal */ export const retry: { - ( - policy: Schedule.Schedule + >( + options: O + ): ( + self: Client.HttpClient + ) => Client.Retry.Return + ( + policy: Schedule.Schedule, R1> ): (self: Client.HttpClient) => Client.HttpClient - ( + >( self: Client.HttpClient, - policy: Schedule.Schedule - ): Client.HttpClient + options: O + ): Client.Retry.Return + ( + self: Client.HttpClient, + policy: Schedule.Schedule + ): Client.HttpClient } = dual( 2, ( @@ -683,7 +670,7 @@ export const schemaFunction = dual< }) }), (body) => - self( + self.execute( internalRequest.setBody( request, internalBody.uint8Array(body, "application/json") @@ -714,7 +701,10 @@ export const tapRequest = dual< self: Client.HttpClient, f: (a: ClientRequest.HttpClientRequest) => Effect.Effect<_, E2, R2> ) => Client.HttpClient ->(2, (self, f) => make(self.execute as any, (request) => Effect.tap(self.preprocess(request), f))) +>(2, (self, f) => { + const client = self as HttpClientImpl + return make(client.postprocess as any, (request) => Effect.tap(client.preprocess(request), f)) +}) /** @internal */ export const withCookiesRef = dual< @@ -730,15 +720,16 @@ export const withCookiesRef = dual< ( self: Client.HttpClient.WithResponse, ref: Ref.Ref - ): Client.HttpClient.WithResponse => - make( + ): Client.HttpClient.WithResponse => { + const client = self as HttpClientImpl + return make( (request: Effect.Effect) => Effect.tap( - self.execute(request), + client.postprocess(request), (response) => Ref.update(ref, (cookies) => Cookies.merge(cookies, response.cookies)) ), (request) => - Effect.flatMap(self.preprocess(request), (request) => + Effect.flatMap(client.preprocess(request), (request) => Effect.map( Ref.get(ref), (cookies) => @@ -747,6 +738,7 @@ export const withCookiesRef = dual< : internalRequest.setHeader(request, "cookie", Cookies.toCookieHeader(cookies)) )) ) + } ) /** @internal */ @@ -761,15 +753,16 @@ export const followRedirects = dual< >((args) => isClient(args[0]), ( self: Client.HttpClient.WithResponse, maxRedirects?: number | undefined -): Client.HttpClient.WithResponse => - make( +): Client.HttpClient.WithResponse => { + const client = self as HttpClientImpl + return make( (request) => { const loop = ( request: ClientRequest.HttpClientRequest, redirects: number ): Effect.Effect => Effect.flatMap( - self.execute(Effect.succeed(request)), + client.postprocess(Effect.succeed(request)), (response) => response.status >= 300 && response.status < 400 && response.headers.location && redirects < (maxRedirects ?? 10) @@ -784,5 +777,18 @@ export const followRedirects = dual< ) return Effect.flatMap(request, (request) => loop(request, 0)) }, - self.preprocess - )) + client.preprocess + ) +}) + +/** @internal */ +export const layerMergedContext = (effect: Effect.Effect) => + Layer.effect( + tag, + Effect.flatMap(Effect.context(), (context) => + Effect.map(effect, (client) => + transformResponse( + client, + Effect.mapInputContext((input: Context.Context) => Context.merge(context, input)) + ))) + ) diff --git a/packages/platform/src/internal/httpClientRequest.ts b/packages/platform/src/internal/httpClientRequest.ts index 70b2b493d3..9ed4683f55 100644 --- a/packages/platform/src/internal/httpClientRequest.ts +++ b/packages/platform/src/internal/httpClientRequest.ts @@ -21,14 +21,14 @@ import * as internalBody from "./httpBody.js" export const TypeId: ClientRequest.TypeId = Symbol.for("@effect/platform/HttpClientRequest") as ClientRequest.TypeId /** @internal */ -export const clientTag = Context.GenericTag("@effect/platform/HttpClient") +export const clientTag = Context.GenericTag("@effect/platform/HttpClient") const Proto = { [TypeId]: TypeId, ...Effectable.CommitPrototype, ...Inspectable.BaseProto, commit(this: ClientRequest.HttpClientRequest) { - return Effect.flatMap(clientTag, (client) => client(this)) + return Effect.flatMap(clientTag, (client) => client.execute(this)) }, toJSON(this: ClientRequest.HttpClientRequest): unknown { return { @@ -393,7 +393,7 @@ export const setBody = dual< }) /** @internal */ -export const uint8ArrayBody = dual< +export const bodyUint8Array = dual< ( body: Uint8Array, contentType?: string @@ -405,7 +405,7 @@ export const uint8ArrayBody = dual< ) /** @internal */ -export const textBody = dual< +export const bodyText = dual< (body: string, contentType?: string) => (self: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest, (self: ClientRequest.HttpClientRequest, body: string, contentType?: string) => ClientRequest.HttpClientRequest >( @@ -414,7 +414,7 @@ export const textBody = dual< ) /** @internal */ -export const jsonBody = dual< +export const bodyJson = dual< ( body: unknown ) => (self: ClientRequest.HttpClientRequest) => Effect.Effect, @@ -425,13 +425,13 @@ export const jsonBody = dual< >(2, (self, body) => Effect.map(internalBody.json(body), (body) => setBody(self, body))) /** @internal */ -export const unsafeJsonBody = dual< +export const bodyUnsafeJson = dual< (body: unknown) => (self: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest, (self: ClientRequest.HttpClientRequest, body: unknown) => ClientRequest.HttpClientRequest >(2, (self, body) => setBody(self, internalBody.unsafeJson(body))) /** @internal */ -export const fileBody = dual< +export const bodyFile = dual< ( path: string, options?: FileSystem.StreamOptions & { readonly contentType?: string } @@ -449,13 +449,13 @@ export const fileBody = dual< ) /** @internal */ -export const fileWebBody = dual< +export const bodyFileWeb = dual< (file: Body.HttpBody.FileLike) => (self: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest, (self: ClientRequest.HttpClientRequest, file: Body.HttpBody.FileLike) => ClientRequest.HttpClientRequest >(2, (self, file) => setBody(self, internalBody.fileWeb(file))) /** @internal */ -export const schemaBody = (schema: Schema.Schema, options?: ParseOptions | undefined): { +export const schemaBodyJson = (schema: Schema.Schema, options?: ParseOptions | undefined): { ( body: A ): (self: ClientRequest.HttpClientRequest) => Effect.Effect @@ -479,7 +479,7 @@ export const schemaBody = (schema: Schema.Schema, options?: Pa } /** @internal */ -export const urlParamsBody = dual< +export const bodyUrlParams = dual< (input: UrlParams.Input) => (self: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest, (self: ClientRequest.HttpClientRequest, input: UrlParams.Input) => ClientRequest.HttpClientRequest >(2, (self, body) => @@ -492,13 +492,13 @@ export const urlParamsBody = dual< )) /** @internal */ -export const formDataBody = dual< +export const bodyFormData = dual< (body: FormData) => (self: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest, (self: ClientRequest.HttpClientRequest, body: FormData) => ClientRequest.HttpClientRequest >(2, (self, body) => setBody(self, internalBody.formData(body))) /** @internal */ -export const streamBody = dual< +export const bodyStream = dual< ( body: Stream.Stream, options?: { diff --git a/packages/platform/src/internal/httpClientResponse.ts b/packages/platform/src/internal/httpClientResponse.ts index d5ed7f31f8..e71c39f159 100644 --- a/packages/platform/src/internal/httpClientResponse.ts +++ b/packages/platform/src/internal/httpClientResponse.ts @@ -5,8 +5,8 @@ import * as Effect from "effect/Effect" import { dual } from "effect/Function" import * as Inspectable from "effect/Inspectable" import * as Option from "effect/Option" -import type { Scope } from "effect/Scope" import * as Stream from "effect/Stream" +import type { Unify } from "effect/Unify" import * as Cookies from "../Cookies.js" import * as Headers from "../Headers.js" import * as Error from "../HttpClientError.js" @@ -194,63 +194,10 @@ export const schemaNoBody = < }) } -/** @internal */ -export const arrayBuffer = (effect: Effect.Effect) => - Effect.scoped(Effect.flatMap(effect, (_) => _.arrayBuffer)) - -/** @internal */ -export const text = (effect: Effect.Effect) => - Effect.scoped(Effect.flatMap(effect, (_) => _.text)) - -/** @internal */ -export const json = (effect: Effect.Effect) => - Effect.scoped(Effect.flatMap(effect, (_) => _.json)) - -/** @internal */ -export const urlParamsBody = (effect: Effect.Effect) => - Effect.scoped(Effect.flatMap(effect, (_) => _.urlParamsBody)) - -/** @internal */ -export const formData = (effect: Effect.Effect) => - Effect.scoped(Effect.flatMap(effect, (_) => _.formData)) - -/** @internal */ -export const void_ = (effect: Effect.Effect) => - Effect.scoped(Effect.asVoid(effect)) - /** @internal */ export const stream = (effect: Effect.Effect) => Stream.unwrapScoped(Effect.map(effect, (_) => _.stream)) -/** @internal */ -export const schemaJsonScoped = < - R, - I extends { - readonly status?: number | undefined - readonly headers?: Readonly> | undefined - readonly body?: unknown | undefined - }, - A ->(schema: Schema.Schema, options?: ParseOptions | undefined) => { - const decode = schemaJson(schema, options) - return (effect: Effect.Effect) => - Effect.scoped(Effect.flatMap(effect, decode)) -} - -/** @internal */ -export const schemaNoBodyScoped = < - R, - I extends { - readonly status?: number | undefined - readonly headers?: Readonly> | undefined - }, - A ->(schema: Schema.Schema, options?: ParseOptions | undefined) => { - const decode = schemaNoBody(schema, options) - return (effect: Effect.Effect) => - Effect.scoped(Effect.flatMap(effect, decode)) -} - /** @internal */ export const matchStatus = dual< < @@ -264,7 +211,7 @@ export const matchStatus = dual< } >( cases: Cases - ) => (self: ClientResponse.HttpClientResponse) => Cases[keyof Cases] extends (_: any) => infer R ? R : never, + ) => (self: ClientResponse.HttpClientResponse) => Cases[keyof Cases] extends (_: any) => infer R ? Unify : never, < const Cases extends { readonly [status: number]: (_: ClientResponse.HttpClientResponse) => any @@ -274,7 +221,10 @@ export const matchStatus = dual< readonly "5xx"?: (_: ClientResponse.HttpClientResponse) => any readonly orElse: (_: ClientResponse.HttpClientResponse) => any } - >(self: ClientResponse.HttpClientResponse, cases: Cases) => Cases[keyof Cases] extends (_: any) => infer R ? R : never + >( + self: ClientResponse.HttpClientResponse, + cases: Cases + ) => Cases[keyof Cases] extends (_: any) => infer R ? Unify : never >(2, (self, cases) => { const status = self.status if (cases[status]) { @@ -290,43 +240,3 @@ export const matchStatus = dual< } return cases.orElse(self) }) - -/** @internal */ -export const matchStatusScoped = dual< - < - const Cases extends { - readonly [status: number]: (_: ClientResponse.HttpClientResponse) => Effect.Effect - readonly "2xx"?: (_: ClientResponse.HttpClientResponse) => Effect.Effect - readonly "3xx"?: (_: ClientResponse.HttpClientResponse) => Effect.Effect - readonly "4xx"?: (_: ClientResponse.HttpClientResponse) => Effect.Effect - readonly "5xx"?: (_: ClientResponse.HttpClientResponse) => Effect.Effect - readonly orElse: (_: ClientResponse.HttpClientResponse) => Effect.Effect - } - >(cases: Cases) => (self: Effect.Effect) => Effect.Effect< - Cases[keyof Cases] extends (_: any) => Effect.Effect ? _A : never, - E | (Cases[keyof Cases] extends (_: any) => Effect.Effect ? _E : never), - Exclude< - R | (Cases[keyof Cases] extends (_: any) => Effect.Effect ? _R : never), - Scope - > - >, - < - E, - R, - const Cases extends { - readonly [status: number]: (_: ClientResponse.HttpClientResponse) => Effect.Effect - readonly "2xx"?: (_: ClientResponse.HttpClientResponse) => Effect.Effect - readonly "3xx"?: (_: ClientResponse.HttpClientResponse) => Effect.Effect - readonly "4xx"?: (_: ClientResponse.HttpClientResponse) => Effect.Effect - readonly "5xx"?: (_: ClientResponse.HttpClientResponse) => Effect.Effect - readonly orElse: (_: ClientResponse.HttpClientResponse) => Effect.Effect - } - >(self: Effect.Effect, cases: Cases) => Effect.Effect< - Cases[keyof Cases] extends (_: any) => Effect.Effect ? _A : never, - E | (Cases[keyof Cases] extends (_: any) => Effect.Effect ? _E : never), - Exclude< - R | (Cases[keyof Cases] extends (_: any) => Effect.Effect ? _R : never), - Scope - > - > ->(2, (self, cases) => Effect.scoped(Effect.flatMap(self, matchStatus(cases) as any))) diff --git a/packages/platform/test/HttpClient.test.ts b/packages/platform/test/HttpClient.test.ts index 8dc0f03a9f..7ba2484ff2 100644 --- a/packages/platform/test/HttpClient.test.ts +++ b/packages/platform/test/HttpClient.test.ts @@ -1,4 +1,11 @@ -import { Cookies, HttpClient, HttpClientRequest, HttpClientResponse, UrlParams } from "@effect/platform" +import { + Cookies, + FetchHttpClient, + HttpClient, + HttpClientRequest, + HttpClientResponse, + UrlParams +} from "@effect/platform" import * as Schema from "@effect/schema/Schema" import { assert, describe, expect, it } from "@effect/vitest" import { Either, Ref } from "effect" @@ -25,7 +32,8 @@ const makeJsonPlaceholder = Effect.gen(function*(_) { HttpClient.mapRequest(HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com")) ) const todoClient = client.pipe( - HttpClient.mapEffectScoped(HttpClientResponse.schemaBodyJson(Todo)) + HttpClient.mapEffect(HttpClientResponse.schemaBodyJson(Todo)), + HttpClient.scoped ) const createTodo = HttpClient.schemaFunction( todoClient, @@ -40,27 +48,28 @@ const makeJsonPlaceholder = Effect.gen(function*(_) { interface JsonPlaceholder extends Effect.Effect.Success {} const JsonPlaceholder = Context.GenericTag("test/JsonPlaceholder") const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) - .pipe(Layer.provide(HttpClient.layer)) + .pipe(Layer.provide(FetchHttpClient.layer)) describe("HttpClient", () => { it("google", () => Effect.gen(function*(_) { const response = yield* _( HttpClientRequest.get("https://www.google.com/"), - HttpClient.fetchOk, Effect.flatMap((_) => _.text), Effect.scoped ) expect(response).toContain("Google") - }).pipe(Effect.runPromise)) + }).pipe(Effect.provide(FetchHttpClient.layer), Effect.runPromise)) it("google withCookiesRef", () => Effect.gen(function*(_) { const ref = yield* _(Ref.make(Cookies.empty)) - const client = HttpClient.withCookiesRef(HttpClient.fetchOk, ref) + const client = (yield* HttpClient.HttpClient).pipe( + HttpClient.withCookiesRef(ref) + ) yield* _( HttpClientRequest.get("https://www.google.com/"), - client, + client.execute, Effect.scoped ) const cookieHeader = yield* _(Ref.get(ref), Effect.map(Cookies.toCookieHeader)) @@ -72,27 +81,26 @@ describe("HttpClient", () => { assert.strictEqual(req.headers.cookie, cookieHeader) }) ) - ), + ).execute, Effect.scoped ) - }).pipe(Effect.runPromise)) + }).pipe(Effect.provide(FetchHttpClient.layer), Effect.runPromise)) it("google stream", () => Effect.gen(function*(_) { const response = yield* _( HttpClientRequest.get(new URL("https://www.google.com/")), - HttpClient.fetchOk, Effect.map((_) => _.stream), Stream.unwrapScoped, Stream.runFold("", (a, b) => a + new TextDecoder().decode(b)) ) expect(response).toContain("Google") - }).pipe(Effect.runPromise)) + }).pipe(Effect.provide(FetchHttpClient.layer), Effect.runPromise)) it("jsonplaceholder", () => - Effect.gen(function*(_) { - const jp = yield* _(JsonPlaceholder) - const response = yield* _(HttpClientRequest.get("/todos/1"), jp.todoClient) + Effect.gen(function*() { + const jp = yield* JsonPlaceholder + const response = yield* jp.todoClient.get("/todos/1") expect(response.id).toBe(1) }).pipe(Effect.provide(JsonPlaceholderLive), Effect.runPromise)) @@ -110,30 +118,32 @@ describe("HttpClient", () => { it("jsonplaceholder schemaJson", () => Effect.gen(function*(_) { const jp = yield* _(JsonPlaceholder) - const client = HttpClient.mapEffectScoped(jp.client, HttpClientResponse.schemaJson(OkTodo)).pipe( + const client = HttpClient.mapEffect(jp.client, HttpClientResponse.schemaJson(OkTodo)).pipe( + HttpClient.scoped, HttpClient.map((_) => _.body) ) - const response = yield* _(HttpClientRequest.get("/todos/1"), client) + const response = yield* client.get("/todos/1") expect(response.id).toBe(1) }).pipe(Effect.provide(JsonPlaceholderLive), Effect.runPromise)) it("request processing order", () => - Effect.gen(function*(_) { - const defaultClient = yield* _(HttpClient.HttpClient) + Effect.gen(function*() { + const defaultClient = yield* HttpClient.HttpClient const client = defaultClient.pipe( HttpClient.mapRequest(HttpClientRequest.prependUrl("jsonplaceholder.typicode.com")), HttpClient.mapRequest(HttpClientRequest.prependUrl("https://")) ) const todoClient = client.pipe( - HttpClient.mapEffectScoped(HttpClientResponse.schemaBodyJson(Todo)) + HttpClient.mapEffect(HttpClientResponse.schemaBodyJson(Todo)), + HttpClient.scoped ) - const response = yield* _(HttpClientRequest.get("/todos/1"), todoClient) + const response = yield* todoClient.get("/todos/1") expect(response.id).toBe(1) - }).pipe(Effect.provide(HttpClient.layer), Effect.runPromise)) + }).pipe(Effect.provide(FetchHttpClient.layer), Effect.runPromise)) it("streamBody accesses the current runtime", () => - Effect.gen(function*(_) { - const defaultClient = yield* _(HttpClient.HttpClient) + Effect.gen(function*() { + const defaultClient = yield* HttpClient.HttpClient const requestStream = Stream.fromIterable(["hello", "world"]).pipe( Stream.tap((_) => Effect.log(_)), @@ -144,14 +154,14 @@ describe("HttpClient", () => { const logger = Logger.make(({ message }) => logs.push(message)) yield* HttpClientRequest.post("https://jsonplaceholder.typicode.com").pipe( - HttpClientRequest.streamBody(requestStream), - defaultClient, + HttpClientRequest.bodyStream(requestStream), + defaultClient.execute, Effect.provide(Logger.replace(Logger.defaultLogger, logger)), Effect.scoped ) expect(logs).toEqual([["hello"], ["world"]]) - }).pipe(Effect.provide(HttpClient.layer), Effect.runPromise)) + }).pipe(Effect.provide(FetchHttpClient.layer), Effect.runPromise)) it("ClientRequest parses URL instances", () => { const request = HttpClientRequest.get(new URL("https://example.com/?foo=bar#hash")).pipe( @@ -164,16 +174,18 @@ describe("HttpClient", () => { ) }) - it.effect("matchStatusScoped", () => + it.effect("matchStatus", () => Effect.gen(function*() { const jp = yield* JsonPlaceholder - const response = yield* HttpClientRequest.get("/todos/1").pipe( - jp.client, - HttpClientResponse.matchStatusScoped({ - "2xx": HttpClientResponse.schemaBodyJson(Todo), - 404: () => Effect.fail("not found"), - orElse: () => Effect.fail("boom") - }) + const response = yield* jp.client.get("/todos/1").pipe( + Effect.flatMap( + HttpClientResponse.matchStatus({ + "2xx": HttpClientResponse.schemaBodyJson(Todo), + 404: () => Effect.fail("not found"), + orElse: () => Effect.fail("boom") + }) + ), + Effect.scoped ) assert.deepStrictEqual(response, { id: 1, userId: 1, title: "delectus aut autem", completed: false }) }).pipe(Effect.provide(JsonPlaceholderLive))) diff --git a/packages/rpc-http/examples/client.ts b/packages/rpc-http/examples/client.ts index f54966b862..f69b8d9901 100644 --- a/packages/rpc-http/examples/client.ts +++ b/packages/rpc-http/examples/client.ts @@ -1,4 +1,4 @@ -import { HttpClient, HttpClientRequest } from "@effect/platform" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "@effect/platform" import { RpcResolver } from "@effect/rpc" import { HttpRpcResolver } from "@effect/rpc-http" import { Console, Effect, Stream } from "effect" @@ -6,16 +6,24 @@ import type { UserRouter } from "./router.js" import { GetUser, GetUserIds } from "./schema.js" // Create the client -const client = HttpRpcResolver.make( - HttpClient.fetchOk.pipe( - HttpClient.mapRequest(HttpClientRequest.prependUrl("http://localhost:3000/rpc")) - ) -).pipe(RpcResolver.toClient) +const makeClient = Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + return HttpRpcResolver.make( + client.pipe( + HttpClient.mapRequest(HttpClientRequest.prependUrl("http://localhost:3000/rpc")) + ) + ).pipe(RpcResolver.toClient) +}) // Use the client -client(new GetUserIds()).pipe( - Stream.runCollect, - Effect.flatMap(Effect.forEach((id) => client(new GetUser({ id })), { batching: true })), - Effect.tap(Console.log), +Effect.gen(function*() { + const client = yield* makeClient + yield* client(new GetUserIds()).pipe( + Stream.runCollect, + Effect.flatMap(Effect.forEach((id) => client(new GetUser({ id })), { batching: true })), + Effect.tap(Console.log) + ) +}).pipe( + Effect.provide(FetchHttpClient.layer), Effect.runFork ) diff --git a/packages/rpc-http/src/HttpRpcResolver.ts b/packages/rpc-http/src/HttpRpcResolver.ts index 6d00d43b6b..76caed75b8 100644 --- a/packages/rpc-http/src/HttpRpcResolver.ts +++ b/packages/rpc-http/src/HttpRpcResolver.ts @@ -19,15 +19,15 @@ import * as Stream from "effect/Stream" * @since 1.0.0 */ export const make = >( - client: Client.HttpClient.Default + client: Client.HttpClient.Service ): RequestResolver.RequestResolver< Rpc.Request>, Serializable.SerializableWithResult.Context> > => Resolver.make((requests) => - client(ClientRequest.post("", { + client.post("", { body: Body.unsafeJson(requests) - })).pipe( + }).pipe( Effect.map((_) => _.stream.pipe( Stream.decodeText(), @@ -46,19 +46,22 @@ export const make = >( */ export const makeClient = >( baseUrl: string -): Serializable.SerializableWithResult.Context> extends never ? Resolver.Client< - RequestResolver.RequestResolver< - Rpc.Request> - > - > : - "HttpResolver.makeClient: request context is not `never`" => - Resolver.toClient(make( - Client.fetchOk.pipe( +): Serializable.SerializableWithResult.Context> extends never ? Effect.Effect< + Resolver.Client< + RequestResolver.RequestResolver< + Rpc.Request> + > + >, + never, + Client.HttpClient + > + : "request context is not `never`" => + Effect.map(Client.HttpClient, (client) => + Resolver.toClient(make(client.pipe( Client.mapRequest(ClientRequest.prependUrl(baseUrl)), Client.retry( Schedule.exponential(50).pipe( Schedule.intersect(Schedule.recurs(5)) ) ) - ) - ) as any) as any + )) as any)) as any diff --git a/packages/rpc-http/src/HttpRpcResolverNoStream.ts b/packages/rpc-http/src/HttpRpcResolverNoStream.ts index 4668adce19..746b8a6243 100644 --- a/packages/rpc-http/src/HttpRpcResolverNoStream.ts +++ b/packages/rpc-http/src/HttpRpcResolverNoStream.ts @@ -18,15 +18,15 @@ import * as Schedule from "effect/Schedule" * @since 1.0.0 */ export const make = >( - client: Client.HttpClient.Default + client: Client.HttpClient.Service ): RequestResolver.RequestResolver< Rpc.Request>, Serializable.SerializableWithResult.Context> > => ResolverNoStream.make((requests) => - client(ClientRequest.post("", { + client.post("", { body: Body.unsafeJson(requests) - })).pipe( + }).pipe( Effect.flatMap((_) => _.json), Effect.scoped ) @@ -38,19 +38,22 @@ export const make = >( */ export const makeClient = >( baseUrl: string -): Serializable.SerializableWithResult.Context> extends never ? Resolver.Client< - RequestResolver.RequestResolver< - Rpc.Request> - > +): Serializable.SerializableWithResult.Context> extends never ? Effect.Effect< + Resolver.Client< + RequestResolver.RequestResolver< + Rpc.Request> + > + >, + never, + Client.HttpClient > - : "HttpResolver.makeClientEffect: request context is not `never`" => - Resolver.toClient(make( - Client.fetchOk.pipe( + : "request context is not `never`" => + Effect.map(Client.HttpClient, (client) => + Resolver.toClient(make(client.pipe( Client.mapRequest(ClientRequest.prependUrl(baseUrl)), Client.retry( Schedule.exponential(50).pipe( Schedule.intersect(Schedule.recurs(5)) ) ) - ) - ) as any) as any + )) as any)) as any From f370c8f4a05b5ace5b976a5c45658b60714579a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Fran=C3=A7ois?= Date: Wed, 4 Sep 2024 10:24:43 +0200 Subject: [PATCH 03/22] Add number round (#3533) --- .changeset/quick-roses-warn.md | 6 ++++++ packages/cli/src/internal/prompt/number.ts | 8 ++------ packages/effect/src/Number.ts | 23 ++++++++++++++++++++++ packages/effect/test/Number.test.ts | 9 +++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 .changeset/quick-roses-warn.md diff --git a/.changeset/quick-roses-warn.md b/.changeset/quick-roses-warn.md new file mode 100644 index 0000000000..bc59fe60d3 --- /dev/null +++ b/.changeset/quick-roses-warn.md @@ -0,0 +1,6 @@ +--- +"effect": minor +"@effect/cli": patch +--- + +Add Number.round diff --git a/packages/cli/src/internal/prompt/number.ts b/packages/cli/src/internal/prompt/number.ts index fc7442eb7f..f2d69334bf 100644 --- a/packages/cli/src/internal/prompt/number.ts +++ b/packages/cli/src/internal/prompt/number.ts @@ -5,6 +5,7 @@ import * as Optimize from "@effect/printer/Optimize" import * as Schema from "@effect/schema/Schema" import * as Arr from "effect/Array" import * as Effect from "effect/Effect" +import * as EffectNumber from "effect/Number" import * as Option from "effect/Option" import type * as Prompt from "../../Prompt.js" import * as InternalPrompt from "../prompt.js" @@ -20,11 +21,6 @@ interface State { readonly error: Option.Option } -const round = (number: number, precision: number) => { - const factor = Math.pow(10, precision) - return Math.round(number * factor) / factor -} - const parseInt = Schema.NumberFromString.pipe( Schema.int(), Schema.decodeUnknown @@ -352,7 +348,7 @@ function handleProcessFloat(options: FloatOptions) { })), onSuccess: (n) => Effect.flatMap( - Effect.sync(() => round(n, options.precision)), + Effect.sync(() => EffectNumber.round(n, options.precision)), (rounded) => Effect.match(options.validate(rounded), { onFailure: (error) => diff --git a/packages/effect/src/Number.ts b/packages/effect/src/Number.ts index bd3181545b..ad5ea1a8e6 100644 --- a/packages/effect/src/Number.ts +++ b/packages/effect/src/Number.ts @@ -492,3 +492,26 @@ export const parse = (s: string): Option => { ? option.none : option.some(n) } + +/** + * Returns the number rounded with the given precision. + * + * @param self - The number to round + * @param precision - The precision + * + * @example + * import { round } from "effect/Number" + * + * assert.deepStrictEqual(round(1.1234, 2), 1.12) + * assert.deepStrictEqual(round(1.567, 2), 1.57) + * + * @category math + * @since 3.8.0 + */ +export const round: { + (precision: number): (self: number) => number + (self: number, precision: number): number +} = dual(2, (self: number, precision: number): number => { + const factor = Math.pow(10, precision) + return Math.round(self * factor) / factor +}) diff --git a/packages/effect/test/Number.test.ts b/packages/effect/test/Number.test.ts index 3e8f9c07e7..21b89b3347 100644 --- a/packages/effect/test/Number.test.ts +++ b/packages/effect/test/Number.test.ts @@ -135,4 +135,13 @@ describe("Number", () => { assert.deepStrictEqual(Number.parse("42"), Option.some(42)) assert.deepStrictEqual(Number.parse("a"), Option.none()) }) + + it("round", () => { + assert.deepStrictEqual(Number.round(1.1234, 2), 1.12) + assert.deepStrictEqual(Number.round(2)(1.1234), 1.12) + assert.deepStrictEqual(Number.round(0)(1.1234), 1) + assert.deepStrictEqual(Number.round(0)(1.1234), 1) + assert.deepStrictEqual(Number.round(1.567, 2), 1.57) + assert.deepStrictEqual(Number.round(2)(1.567), 1.57) + }) }) From 27ec58403616b905f815f1b60471875a028e96a5 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 6 Sep 2024 11:31:03 +1200 Subject: [PATCH 04/22] add Logger.prettyLoggerDefault, to prevent duplicate pretty loggers (#3552) --- .changeset/honest-cups-wash.md | 6 ++++++ packages/effect/src/Logger.ts | 8 ++++++++ packages/effect/src/internal/fiberRuntime.ts | 2 +- packages/effect/src/internal/logger.ts | 4 ++++ packages/platform/src/Runtime.ts | 2 +- 5 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 .changeset/honest-cups-wash.md diff --git a/.changeset/honest-cups-wash.md b/.changeset/honest-cups-wash.md new file mode 100644 index 0000000000..788db95a1d --- /dev/null +++ b/.changeset/honest-cups-wash.md @@ -0,0 +1,6 @@ +--- +"effect": minor +"@effect/platform": patch +--- + +add Logger.prettyLoggerDefault, to prevent duplicate pretty loggers diff --git a/packages/effect/src/Logger.ts b/packages/effect/src/Logger.ts index 9bc058c128..29eb1300ac 100644 --- a/packages/effect/src/Logger.ts +++ b/packages/effect/src/Logger.ts @@ -475,6 +475,14 @@ export const prettyLogger: ( } ) => Logger = internal.prettyLogger +/** + * A default version of the pretty logger. + * + * @since 3.8.0 + * @category constructors + */ +export const prettyLoggerDefault: Logger = internal.prettyLoggerDefault + /** * The structured logger provides detailed log outputs, structured in a way that * retains comprehensive traceability of the events, suitable for deeper diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index a4b99c3980..e2cac6688d 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -1426,7 +1426,7 @@ export const logFmtLogger: Logger = globalValue( /** @internal */ export const prettyLogger: Logger = globalValue( Symbol.for("effect/Logger/prettyLogger"), - () => internalLogger.prettyLogger() + () => internalLogger.prettyLoggerDefault ) /** @internal */ diff --git a/packages/effect/src/internal/logger.ts b/packages/effect/src/internal/logger.ts index 5fc774c961..5bb919236a 100644 --- a/packages/effect/src/internal/logger.ts +++ b/packages/effect/src/internal/logger.ts @@ -3,6 +3,7 @@ import * as Context from "../Context.js" import * as FiberRefs from "../FiberRefs.js" import type { LazyArg } from "../Function.js" import { constVoid, dual, pipe } from "../Function.js" +import { globalValue } from "../GlobalValue.js" import * as HashMap from "../HashMap.js" import * as Inspectable from "../Inspectable.js" import * as List from "../List.js" @@ -570,3 +571,6 @@ const prettyLoggerBrowser = (options: { } ) } + +/** @internal */ +export const prettyLoggerDefault = globalValue("effect/Logger/prettyLoggerDefault", () => prettyLogger()) diff --git a/packages/platform/src/Runtime.ts b/packages/platform/src/Runtime.ts index 6481cf417f..223e5aee08 100644 --- a/packages/platform/src/Runtime.ts +++ b/packages/platform/src/Runtime.ts @@ -63,7 +63,7 @@ const addPrettyLogger = (refs: FiberRefs.FiberRefs, fiberId: FiberId.Runtime) => fiberRef: FiberRef.currentLoggers, value: loggers.pipe( HashSet.remove(Logger.defaultLogger), - HashSet.add(Logger.prettyLogger()) + HashSet.add(Logger.prettyLoggerDefault) ) }) } From be4e68da6fc5c98905ce8984fd378ea140b9ee7b Mon Sep 17 00:00:00 2001 From: Sebastian Lorenz Date: Sat, 7 Sep 2024 07:03:35 +0200 Subject: [PATCH 05/22] Add `RcMap.keys` and `MutableHashMap.keys` lookup functions (#3559) Co-authored-by: Tim Co-authored-by: Tim Smart --- .changeset/violet-suns-chew.md | 26 +++++++++++++++++++++ packages/effect/src/MutableHashMap.ts | 12 ++++++++++ packages/effect/src/RcMap.ts | 6 +++++ packages/effect/src/internal/rcMap.ts | 8 +++++++ packages/effect/test/MutableHashMap.test.ts | 13 +++++++++++ packages/effect/test/RcMap.test.ts | 13 +++++++++++ 6 files changed, 78 insertions(+) create mode 100644 .changeset/violet-suns-chew.md diff --git a/.changeset/violet-suns-chew.md b/.changeset/violet-suns-chew.md new file mode 100644 index 0000000000..6371200bb7 --- /dev/null +++ b/.changeset/violet-suns-chew.md @@ -0,0 +1,26 @@ +--- +"effect": minor +--- + +Added `RcMap.keys` and `MutableHashMap.keys`. + +These functions allow you to get a list of keys currently stored in the underlying hash map. + +```ts +const map = MutableHashMap.make([["a", "a"], ["b", "b"], ["c", "c"]]) +const keys = MutableHashMap.keys(map) // ["a", "b", "c"] +``` + +```ts +Effect.gen(function* () { + const map = yield* RcMap.make({ + lookup: (key) => Effect.succeed(key) + }) + + yield* RcMap.get(map, "a") + yield* RcMap.get(map, "b") + yield* RcMap.get(map, "c") + + const keys = yield* RcMap.keys(map) // ["a", "b", "c"] +}) +``` diff --git a/packages/effect/src/MutableHashMap.ts b/packages/effect/src/MutableHashMap.ts index 16071b33d5..1b5e0cabbb 100644 --- a/packages/effect/src/MutableHashMap.ts +++ b/packages/effect/src/MutableHashMap.ts @@ -159,6 +159,18 @@ export const get: { return getFromBucket(self, bucket, key) }) +/** + * @since 3.8.0 + * @category elements + */ +export const keys = (self: MutableHashMap): Array => { + const keys: Array = [] + for (const [key] of self) { + keys.push(key) + } + return keys +} + const getFromBucket = ( self: MutableHashMap, bucket: NonEmptyArray, diff --git a/packages/effect/src/RcMap.ts b/packages/effect/src/RcMap.ts index 54f0170da7..ed71100139 100644 --- a/packages/effect/src/RcMap.ts +++ b/packages/effect/src/RcMap.ts @@ -101,3 +101,9 @@ export const get: { (key: K): (self: RcMap) => Effect.Effect (self: RcMap, key: K): Effect.Effect } = internal.get + +/** + * @since 3.8.0 + * @category combinators + */ +export const keys: (self: RcMap) => Effect.Effect, E> = internal.keys diff --git a/packages/effect/src/internal/rcMap.ts b/packages/effect/src/internal/rcMap.ts index 566d8e9476..d1fece6c77 100644 --- a/packages/effect/src/internal/rcMap.ts +++ b/packages/effect/src/internal/rcMap.ts @@ -211,3 +211,11 @@ export const get: { ) } ) + +/** @internal */ +export const keys = (self: RcMap.RcMap): Effect> => { + const impl = self as RcMapImpl + return core.suspend(() => + impl.state._tag === "Closed" ? core.interrupt : core.succeed(MutableHashMap.keys(impl.state.map)) + ) +} diff --git a/packages/effect/test/MutableHashMap.test.ts b/packages/effect/test/MutableHashMap.test.ts index 5ed9cac91a..d797145450 100644 --- a/packages/effect/test/MutableHashMap.test.ts +++ b/packages/effect/test/MutableHashMap.test.ts @@ -174,6 +174,19 @@ describe("MutableHashMap", () => { ) }) + it("keys", () => { + const map = pipe( + HM.empty(), + HM.set(key(0, 0), value(0, 0)), + HM.set(key(1, 1), value(1, 1)) + ) + + expect(HM.keys(map)).toStrictEqual([ + key(0, 0), + key(1, 1) + ]) + }) + it("modifyAt", () => { const map = pipe( HM.empty(), diff --git a/packages/effect/test/RcMap.test.ts b/packages/effect/test/RcMap.test.ts index 7e353879f7..52455135f9 100644 --- a/packages/effect/test/RcMap.test.ts +++ b/packages/effect/test/RcMap.test.ts @@ -124,4 +124,17 @@ describe("RcMap", () => { // no failure means a hit assert.strictEqual(yield* RcMap.get(map, new Key({ id: 1 })), 1) })) + + it.scoped("keys lookup", () => + Effect.gen(function*() { + const map = yield* RcMap.make({ + lookup: (key: string) => Effect.succeed(key) + }) + + yield* RcMap.get(map, "foo") + yield* RcMap.get(map, "bar") + yield* RcMap.get(map, "baz") + + assert.deepStrictEqual(yield* RcMap.keys(map), ["foo", "bar", "baz"]) + })) }) From 02e2d79a442055e8fd894cfe77763cc9d16e695d Mon Sep 17 00:00:00 2001 From: Maxim Khramtsov Date: Sun, 8 Sep 2024 05:01:59 +0200 Subject: [PATCH 06/22] Made `Ref`, `SynchronizedRef`,`RcRef, and `SubscriptionRef` a subtype of `Effect` (#3511) Co-authored-by: maksim.khramtsov --- .changeset/eighty-lobsters-refuse.md | 5 ++ packages/effect/dtslint/Unify.ts | 61 ++++++++++++++++--- packages/effect/src/RcRef.ts | 26 +++++++- packages/effect/src/Ref.ts | 24 +++++++- packages/effect/src/SubscriptionRef.ts | 22 +++++++ packages/effect/src/SynchronizedRef.ts | 20 ++++++ .../effect/src/internal/effect/circular.ts | 13 ++-- packages/effect/src/internal/rcRef.ts | 16 +++-- packages/effect/src/internal/ref.ts | 14 ++--- .../effect/src/internal/subscriptionRef.ts | 15 +++-- packages/effect/test/RcRef.test.ts | 10 +-- packages/effect/test/Ref.test.ts | 13 ++-- packages/effect/test/Synchronized.test.ts | 10 +-- 13 files changed, 195 insertions(+), 54 deletions(-) create mode 100644 .changeset/eighty-lobsters-refuse.md diff --git a/.changeset/eighty-lobsters-refuse.md b/.changeset/eighty-lobsters-refuse.md new file mode 100644 index 0000000000..571aff3b76 --- /dev/null +++ b/.changeset/eighty-lobsters-refuse.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +Made `Ref`, `SynchronizedRed` and `SubscriptionRef` a subtype of `Effect` diff --git a/packages/effect/dtslint/Unify.ts b/packages/effect/dtslint/Unify.ts index 4aabe4784d..b5d13be156 100644 --- a/packages/effect/dtslint/Unify.ts +++ b/packages/effect/dtslint/Unify.ts @@ -1,18 +1,23 @@ import type * as Effect from "effect/Effect" import * as Either from "effect/Either" +import type * as Exit from "effect/Exit" import type * as Micro from "effect/Micro" import type * as Option from "effect/Option" +import type * as RcRef from "effect/RcRef" +import type * as Ref from "effect/Ref" import type * as Stream from "effect/Stream" +import type * as SubscriptionRef from "effect/SubscriptionRef" +import type * as SynchronizedRef from "effect/SynchronizedRef" import * as Unify from "effect/Unify" // $ExpectType Option -export type option = Unify.Unify | Option.Option> +export type OptionUnify = Unify.Unify | Option.Option> // $ExpectType Either<"RA" | "RB", "LA" | "LB"> -export type either = Unify.Unify | Either.Either<"RB", "LB">> +export type EitherUnify = Unify.Unify | Either.Either<"RB", "LB">> // $ExpectType 0 | Option | Either<"RA" | "RB", "LA" | "LB"> -export type both = Unify.Unify< +export type EitherOptionUnify = Unify.Unify< Either.Either<"RA", "LA"> | Either.Either<"RB", "LB"> | Option.Option | Option.Option | 0 > @@ -26,15 +31,57 @@ Unify.unify((n: N) => Math.random() > 0 ? Either.right(n) : Either.left("ok") Unify.unify(Math.random() > 0 ? Either.right(10) : Either.left("ok")) // $ExpectType Stream<0 | "a", "b" | 1, "c" | 2> -export type SU = Unify.Unify< +export type StreamUnify = Unify.Unify< Stream.Stream<0, 1, 2> | Stream.Stream<"a", "b", "c"> > // $ExpectType Micro<0 | "a", "b" | 1, "c" | 2> -export type MU = Unify.Unify< +export type MicroUnify = Unify.Unify< Micro.Micro<0, 1, 2> | Micro.Micro<"a", "b", "c"> > // $ExpectType Effect<0 | "a", "b" | 1, "c" | 2> -export type EU = Unify.Unify< - Effect.Effect<0, 1, 2> | Effect.Effect<"a", "b", "c"> +export type EffectUnify = Unify.Unify< + | Effect.Effect<0, 1, 2> + | Effect.Effect<"a", "b", "c"> +> +// $ExpectType Exit<0 | "a", "b" | 1> +export type ExitUnify = Unify.Unify< + | Exit.Exit<0, 1> + | Exit.Exit<"a", "b"> +> +// $ExpectType Ref<1> | Ref<"a"> +export type RefUnify = Unify.Unify | Ref.Ref<"a">> +// $ExpectType SynchronizedRef<1> | SynchronizedRef<"a"> +export type SynchronizedRefUnify = Unify.Unify< + | SynchronizedRef.SynchronizedRef<1> + | SynchronizedRef.SynchronizedRef<"a"> +> +// $ExpectType SubscriptionRef<1> | SubscriptionRef<"a"> +export type SubscriptionRefUnify = Unify.Unify< + | SubscriptionRef.SubscriptionRef<1> + | SubscriptionRef.SubscriptionRef<"a"> +> +// $ExpectType RcRef<"a" | 1, "b" | 2> +export type RcRefUnify = Unify.Unify< + | RcRef.RcRef<1, 2> + | RcRef.RcRef<"a", "b"> +> + +// $ExpectType 0 | Option | Ref<1> | SynchronizedRef<1> | SubscriptionRef<1> | Ref<"A"> | SynchronizedRef<"A"> | SubscriptionRef<"A"> | Either<1 | "A", 0 | "E"> | Effect<1 | "A", 0 | "E", "R" | "R1"> | RcRef<1 | "A", 0 | "E"> +export type AllUnify = Unify.Unify< + | Either.Either<1, 0> + | Either.Either<"A", "E"> + | Option.Option + | Option.Option + | Effect.Effect<"A", "E", "R"> + | Effect.Effect<1, 0, "R1"> + | Ref.Ref<1> + | Ref.Ref<"A"> + | SynchronizedRef.SynchronizedRef<1> + | SynchronizedRef.SynchronizedRef<"A"> + | SubscriptionRef.SubscriptionRef<1> + | SubscriptionRef.SubscriptionRef<"A"> + | RcRef.RcRef<1, 0> + | RcRef.RcRef<"A", "E"> + | 0 > diff --git a/packages/effect/src/RcRef.ts b/packages/effect/src/RcRef.ts index bbdf780a5e..e014272d9f 100644 --- a/packages/effect/src/RcRef.ts +++ b/packages/effect/src/RcRef.ts @@ -4,9 +4,10 @@ import type * as Duration from "./Duration.js" import type * as Effect from "./Effect.js" import * as internal from "./internal/rcRef.js" -import { type Pipeable } from "./Pipeable.js" +import type * as Readable from "./Readable.js" import type * as Scope from "./Scope.js" import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" /** * @since 3.5.0 @@ -24,10 +25,31 @@ export type TypeId = typeof TypeId * @since 3.5.0 * @category models */ -export interface RcRef extends Pipeable { +export interface RcRef + extends Effect.Effect, Readable.Readable +{ readonly [TypeId]: RcRef.Variance + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: RcRefUnify + readonly [Unify.ignoreSymbol]?: RcRefUnifyIgnore } +/** + * @category models + * @since 3.8.0 + */ +export interface RcRefUnify extends Effect.EffectUnify { + RcRef?: () => A[Unify.typeSymbol] extends RcRef | infer _ ? RcRef + : never +} + +/** + * @category models + * @since 3.8.0 + */ +export interface RcRefUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true +} /** * @since 3.5.0 * @category models diff --git a/packages/effect/src/Ref.ts b/packages/effect/src/Ref.ts index 09be40ce7b..0f63baef06 100644 --- a/packages/effect/src/Ref.ts +++ b/packages/effect/src/Ref.ts @@ -4,8 +4,9 @@ import type * as Effect from "./Effect.js" import * as internal from "./internal/ref.js" import type * as Option from "./Option.js" -import type { Readable } from "./Readable.js" +import type * as Readable from "./Readable.js" import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" /** * @since 2.0.0 @@ -23,8 +24,27 @@ export type RefTypeId = typeof RefTypeId * @since 2.0.0 * @category models */ -export interface Ref extends Ref.Variance, Readable { +export interface Ref extends Ref.Variance, Effect.Effect, Readable.Readable { modify(f: (a: A) => readonly [B, A]): Effect.Effect + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: RefUnify + readonly [Unify.ignoreSymbol]?: RefUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface RefUnify extends Effect.EffectUnify { + Ref?: () => Extract> +} + +/** + * @category models + * @since 3.8.0 + */ +export interface RefUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true } /** diff --git a/packages/effect/src/SubscriptionRef.ts b/packages/effect/src/SubscriptionRef.ts index 36850a864e..aac1045a5b 100644 --- a/packages/effect/src/SubscriptionRef.ts +++ b/packages/effect/src/SubscriptionRef.ts @@ -10,6 +10,7 @@ import type * as Stream from "./Stream.js" import type { Subscribable } from "./Subscribable.js" import * as Synchronized from "./SynchronizedRef.js" import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" /** * @since 2.0.0 @@ -44,6 +45,27 @@ export interface SubscriptionRef * to that value. */ readonly changes: Stream.Stream + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: SubscriptionRefUnify + readonly [Unify.ignoreSymbol]?: SubscriptionRefUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface SubscriptionRefUnify + extends Synchronized.SynchronizedRefUnify +{ + SubscriptionRef?: () => Extract> +} + +/** + * @category models + * @since 3.8.0 + */ +export interface SubscriptionRefUnifyIgnore extends Synchronized.SynchronizedRefUnifyIgnore { + SynchronizedRef?: true } /** diff --git a/packages/effect/src/SynchronizedRef.ts b/packages/effect/src/SynchronizedRef.ts index a125fb8202..cf763ec0ba 100644 --- a/packages/effect/src/SynchronizedRef.ts +++ b/packages/effect/src/SynchronizedRef.ts @@ -8,6 +8,7 @@ import * as internal from "./internal/synchronizedRef.js" import type * as Option from "./Option.js" import type * as Ref from "./Ref.js" import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" /** * @since 2.0.0 @@ -27,6 +28,25 @@ export type SynchronizedRefTypeId = typeof SynchronizedRefTypeId */ export interface SynchronizedRef extends SynchronizedRef.Variance, Ref.Ref { modifyEffect(f: (a: A) => Effect.Effect): Effect.Effect + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: SynchronizedRefUnify + readonly [Unify.ignoreSymbol]?: SynchronizedRefUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface SynchronizedRefUnify extends Ref.RefUnify { + SynchronizedRef?: () => Extract> +} + +/** + * @category models + * @since 3.8.0 + */ +export interface SynchronizedRefUnifyIgnore extends Ref.RefUnifyIgnore { + Ref?: true } /** diff --git a/packages/effect/src/internal/effect/circular.ts b/packages/effect/src/internal/effect/circular.ts index 5a3fdbd3c1..da384e5457 100644 --- a/packages/effect/src/internal/effect/circular.ts +++ b/packages/effect/src/internal/effect/circular.ts @@ -2,6 +2,7 @@ import type * as Cause from "../../Cause.js" import type * as Deferred from "../../Deferred.js" import * as Duration from "../../Duration.js" import type * as Effect from "../../Effect.js" +import * as Effectable from "../../Effectable.js" import * as Equal from "../../Equal.js" import type { Equivalence } from "../../Equivalence.js" import * as Exit from "../../Exit.js" @@ -571,18 +572,21 @@ export const synchronizedVariance = { } /** @internal */ -class SynchronizedImpl implements Synchronized.SynchronizedRef { +class SynchronizedImpl extends Effectable.Class implements Synchronized.SynchronizedRef { readonly [SynchronizedTypeId] = synchronizedVariance readonly [internalRef.RefTypeId] = internalRef.refVariance - readonly [Readable.TypeId]: Readable.TypeId + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId constructor( readonly ref: Ref.Ref, readonly withLock: (self: Effect.Effect) => Effect.Effect ) { - this[Readable.TypeId] = Readable.TypeId + super() this.get = internalRef.get(this.ref) } readonly get: Effect.Effect + commit() { + return this.get + } modify(f: (a: A) => readonly [B, A]): Effect.Effect { return this.modifyEffect((a) => core.succeed(f(a))) } @@ -594,9 +598,6 @@ class SynchronizedImpl implements Synchronized.SynchronizedRef { ) ) } - pipe() { - return pipeArguments(this, arguments) - } } /** @internal */ diff --git a/packages/effect/src/internal/rcRef.ts b/packages/effect/src/internal/rcRef.ts index 8fe9841397..daf0b0b88e 100644 --- a/packages/effect/src/internal/rcRef.ts +++ b/packages/effect/src/internal/rcRef.ts @@ -1,10 +1,11 @@ import * as Context from "../Context.js" import * as Duration from "../Duration.js" import type { Effect } from "../Effect.js" +import * as Effectable from "../Effectable.js" import type { RuntimeFiber } from "../Fiber.js" import { identity } from "../Function.js" -import { pipeArguments } from "../Pipeable.js" import type * as RcRef from "../RcRef.js" +import * as Readable from "../Readable.js" import type * as Scope from "../Scope.js" import * as coreEffect from "./core-effect.js" import * as core from "./core.js" @@ -42,8 +43,9 @@ const variance: RcRef.RcRef.Variance = { _E: identity } -class RcRefImpl implements RcRef.RcRef { - readonly [TypeId]: RcRef.RcRef.Variance +class RcRefImpl extends Effectable.Class implements RcRef.RcRef { + readonly [TypeId]: RcRef.RcRef.Variance = variance + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId state: State = stateEmpty readonly semaphore = circular.unsafeMakeSemaphore(1) @@ -54,11 +56,13 @@ class RcRefImpl implements RcRef.RcRef { readonly scope: Scope.Scope, readonly idleTimeToLive: Duration.Duration | undefined ) { - this[TypeId] = variance + super() + this.get = get(this) } + readonly get: Effect - pipe() { - return pipeArguments(this, arguments) + commit() { + return this.get } } diff --git a/packages/effect/src/internal/ref.ts b/packages/effect/src/internal/ref.ts index 666122b8be..cc10f6b3e0 100644 --- a/packages/effect/src/internal/ref.ts +++ b/packages/effect/src/internal/ref.ts @@ -1,8 +1,8 @@ import type * as Effect from "../Effect.js" +import * as Effectable from "../Effectable.js" import { dual } from "../Function.js" import * as MutableRef from "../MutableRef.js" import * as Option from "../Option.js" -import { pipeArguments } from "../Pipeable.js" import * as Readable from "../Readable.js" import type * as Ref from "../Ref.js" import * as core from "./core.js" @@ -16,11 +16,14 @@ export const refVariance = { _A: (_: any) => _ } -class RefImpl implements Ref.Ref { +class RefImpl extends Effectable.Class implements Ref.Ref { + commit() { + return this.get + } readonly [RefTypeId] = refVariance - readonly [Readable.TypeId]: Readable.TypeId + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId constructor(readonly ref: MutableRef.MutableRef) { - this[Readable.TypeId] = Readable.TypeId + super() this.get = core.sync(() => MutableRef.get(this.ref)) } readonly get: Effect.Effect @@ -34,9 +37,6 @@ class RefImpl implements Ref.Ref { return b }) } - pipe() { - return pipeArguments(this, arguments) - } } /** @internal */ diff --git a/packages/effect/src/internal/subscriptionRef.ts b/packages/effect/src/internal/subscriptionRef.ts index e9d08ea672..9b7ad1dec3 100644 --- a/packages/effect/src/internal/subscriptionRef.ts +++ b/packages/effect/src/internal/subscriptionRef.ts @@ -1,6 +1,6 @@ import * as Effect from "../Effect.js" +import * as Effectable from "../Effectable.js" import { dual, pipe } from "../Function.js" -import { pipeArguments } from "../Pipeable.js" import * as PubSub from "../PubSub.js" import * as Readable from "../Readable.js" import * as Ref from "../Ref.js" @@ -26,9 +26,9 @@ const subscriptionRefVariance = { } /** @internal */ -class SubscriptionRefImpl implements SubscriptionRef.SubscriptionRef { - readonly [Readable.TypeId]: Readable.TypeId - readonly [Subscribable.TypeId]: Subscribable.TypeId +class SubscriptionRefImpl extends Effectable.Class implements SubscriptionRef.SubscriptionRef { + readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId + readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId readonly [Ref.RefTypeId] = _ref.refVariance readonly [Synchronized.SynchronizedRefTypeId] = _circular.synchronizedVariance readonly [SubscriptionRefTypeId] = subscriptionRefVariance @@ -37,12 +37,11 @@ class SubscriptionRefImpl implements SubscriptionRef.SubscriptionRef, readonly semaphore: Effect.Semaphore ) { - this[Readable.TypeId] = Readable.TypeId - this[Subscribable.TypeId] = Subscribable.TypeId + super() this.get = Ref.get(this.ref) } - pipe() { - return pipeArguments(this, arguments) + commit() { + return this.get } readonly get: Effect.Effect get changes(): Stream { diff --git a/packages/effect/test/RcRef.test.ts b/packages/effect/test/RcRef.test.ts index c3c439cf2b..02cab8c20b 100644 --- a/packages/effect/test/RcRef.test.ts +++ b/packages/effect/test/RcRef.test.ts @@ -23,14 +23,14 @@ describe("RcRef", () => { ) assert.strictEqual(acquired, 0) - assert.strictEqual(yield* Effect.scoped(RcRef.get(ref)), "foo") + assert.strictEqual(yield* Effect.scoped(ref), "foo") assert.strictEqual(acquired, 1) assert.strictEqual(released, 1) const scopeA = yield* Scope.make() const scopeB = yield* Scope.make() - yield* RcRef.get(ref).pipe(Scope.extend(scopeA)) - yield* RcRef.get(ref).pipe(Scope.extend(scopeB)) + yield* ref.pipe(Scope.extend(scopeA)) + yield* ref.pipe(Scope.extend(scopeB)) assert.strictEqual(acquired, 2) assert.strictEqual(released, 1) yield* Scope.close(scopeB, Exit.void) @@ -41,7 +41,7 @@ describe("RcRef", () => { assert.strictEqual(released, 2) const scopeC = yield* Scope.make() - yield* RcRef.get(ref).pipe(Scope.extend(scopeC)) + yield* ref.pipe(Scope.extend(scopeC)) assert.strictEqual(acquired, 3) assert.strictEqual(released, 2) @@ -49,7 +49,7 @@ describe("RcRef", () => { assert.strictEqual(acquired, 3) assert.strictEqual(released, 3) - const exit = yield* RcRef.get(ref).pipe(Effect.scoped, Effect.exit) + const exit = yield* ref.get.pipe(Effect.scoped, Effect.exit) assert.isTrue(Exit.isInterrupted(exit)) })) diff --git a/packages/effect/test/Ref.test.ts b/packages/effect/test/Ref.test.ts index 45bb563987..92ddfeddeb 100644 --- a/packages/effect/test/Ref.test.ts +++ b/packages/effect/test/Ref.test.ts @@ -35,7 +35,7 @@ describe("Ref", () => { Effect.gen(function*(_) { const ref = yield* _(Ref.make(123)) assert.isTrue(Readable.isReadable(ref)) - assert.strictEqual(yield* _(ref.get), 123) + assert.strictEqual(yield* ref, 123) })) it.effect("get", () => @@ -44,10 +44,11 @@ describe("Ref", () => { assert.strictEqual(result, current) })) it.effect("getAndSet", () => - Effect.gen(function*($) { - const ref = yield* $(Ref.make(current)) - const result1 = yield* $(Ref.getAndSet(ref, update)) - const result2 = yield* $(Ref.get(ref)) + Effect.gen(function*() { + const ref = yield* Ref.make(current) + const result1 = yield* Ref.getAndSet(ref, update) + + const result2 = yield* ref assert.strictEqual(result1, current) assert.strictEqual(result2, update) })) @@ -55,7 +56,7 @@ describe("Ref", () => { Effect.gen(function*($) { const ref = yield* $(Ref.make(current)) const result1 = yield* $(Ref.getAndUpdate(ref, () => update)) - const result2 = yield* $(Ref.get(ref)) + const result2 = yield* ref assert.strictEqual(result1, current) assert.strictEqual(result2, update) })) diff --git a/packages/effect/test/Synchronized.test.ts b/packages/effect/test/Synchronized.test.ts index dec353b198..80f98d743c 100644 --- a/packages/effect/test/Synchronized.test.ts +++ b/packages/effect/test/Synchronized.test.ts @@ -40,10 +40,10 @@ describe("SynchronizedRef", () => { assert.strictEqual(result, current) })) it.effect("getAndUpdateEffect - happy path", () => - Effect.gen(function*($) { - const ref = yield* $(Synchronized.make(current)) - const result1 = yield* $(Synchronized.getAndUpdateEffect(ref, () => Effect.succeed(update))) - const result2 = yield* $(Synchronized.get(ref)) + Effect.gen(function*() { + const ref = yield* Synchronized.make(current) + const result1 = yield* Synchronized.getAndUpdateEffect(ref, () => Effect.succeed(update)) + const result2 = yield* ref assert.strictEqual(result1, current) assert.strictEqual(result2, update) })) @@ -77,7 +77,7 @@ describe("SynchronizedRef", () => { : isChanged(state) ? Option.some(Effect.succeed(Closed)) : Option.none())) - const result3 = yield* $(Synchronized.get(ref)) + const result3 = yield* ref assert.deepStrictEqual(result1, Active) assert.deepStrictEqual(result2, Changed) assert.deepStrictEqual(result3, Closed) From 7e8f0c6f14c7e8f52766eda37c7f7d39af1278b3 Mon Sep 17 00:00:00 2001 From: Maxim Khramtsov Date: Sun, 8 Sep 2024 23:56:23 +0200 Subject: [PATCH 07/22] `Deferred` is subtype of `Effect` (#3572) Co-authored-by: maksim.khramtsov --- .changeset/gorgeous-toes-help.md | 16 ++++++++++++++++ packages/effect/dtslint/Unify.ts | 10 +++++++++- packages/effect/src/Deferred.ts | 23 +++++++++++++++++++++-- packages/effect/src/internal/core.ts | 20 ++++++++++++-------- packages/effect/test/Deferred.test.ts | 10 ++++++++++ 5 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 .changeset/gorgeous-toes-help.md diff --git a/.changeset/gorgeous-toes-help.md b/.changeset/gorgeous-toes-help.md new file mode 100644 index 0000000000..1bc6a01a71 --- /dev/null +++ b/.changeset/gorgeous-toes-help.md @@ -0,0 +1,16 @@ +--- +"effect": minor +--- + +The `Deferred` is now a subtype of `Effect`. This change simplifies handling of deferred values, removing the need for explicit call `Deffer.await`. + +```typescript +import { Effect, Deferred } from "effect" + +Effect.gen(function* () { + const deferred = yield* Deferred.make() + + const before = yield* Deferred.await(deferred) + const after = yield* deferred +}) +``` diff --git a/packages/effect/dtslint/Unify.ts b/packages/effect/dtslint/Unify.ts index b5d13be156..87baa5d34e 100644 --- a/packages/effect/dtslint/Unify.ts +++ b/packages/effect/dtslint/Unify.ts @@ -1,3 +1,4 @@ +import type * as Deferred from "effect/Deferred" import type * as Effect from "effect/Effect" import * as Either from "effect/Either" import type * as Exit from "effect/Exit" @@ -66,8 +67,13 @@ export type RcRefUnify = Unify.Unify< | RcRef.RcRef<1, 2> | RcRef.RcRef<"a", "b"> > +// $ExpectType Deferred<1, 2> | Deferred<"a", "b"> +export type DeferredUnify = Unify.Unify< + | Deferred.Deferred<1, 2> + | Deferred.Deferred<"a", "b"> +> -// $ExpectType 0 | Option | Ref<1> | SynchronizedRef<1> | SubscriptionRef<1> | Ref<"A"> | SynchronizedRef<"A"> | SubscriptionRef<"A"> | Either<1 | "A", 0 | "E"> | Effect<1 | "A", 0 | "E", "R" | "R1"> | RcRef<1 | "A", 0 | "E"> +// $ExpectType 0 | Option | Ref<1> | SynchronizedRef<1> | SubscriptionRef<1> | Deferred<1, 2> | Deferred<"a", "b"> | Ref<"A"> | SynchronizedRef<"A"> | SubscriptionRef<"A"> | Either<1 | "A", 0 | "E"> | Effect<1 | "A", 0 | "E", "R" | "R1"> | RcRef<1 | "A", 0 | "E"> export type AllUnify = Unify.Unify< | Either.Either<1, 0> | Either.Either<"A", "E"> @@ -83,5 +89,7 @@ export type AllUnify = Unify.Unify< | SubscriptionRef.SubscriptionRef<"A"> | RcRef.RcRef<1, 0> | RcRef.RcRef<"A", "E"> + | Deferred.Deferred<1, 2> + | Deferred.Deferred<"a", "b"> | 0 > diff --git a/packages/effect/src/Deferred.ts b/packages/effect/src/Deferred.ts index ea32f09218..10a8cc13d9 100644 --- a/packages/effect/src/Deferred.ts +++ b/packages/effect/src/Deferred.ts @@ -10,8 +10,8 @@ import * as core from "./internal/core.js" import * as internal from "./internal/deferred.js" import type * as MutableRef from "./MutableRef.js" import type * as Option from "./Option.js" -import type { Pipeable } from "./Pipeable.js" import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" /** * @since 2.0.0 @@ -37,11 +37,30 @@ export type DeferredTypeId = typeof DeferredTypeId * @since 2.0.0 * @category models */ -export interface Deferred extends Deferred.Variance, Pipeable { +export interface Deferred extends Effect.Effect, Deferred.Variance { /** @internal */ readonly state: MutableRef.MutableRef> /** @internal */ readonly blockingOn: FiberId.FiberId + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: DeferredUnify + readonly [Unify.ignoreSymbol]?: DeferredUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface DeferredUnify extends Effect.EffectUnify { + Deferred?: () => Extract> +} + +/** + * @category models + * @since 3.8.0 + */ +export interface DeferredUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true } /** diff --git a/packages/effect/src/internal/core.ts b/packages/effect/src/internal/core.ts index fb4e941318..8216dfcd93 100644 --- a/packages/effect/src/internal/core.ts +++ b/packages/effect/src/internal/core.ts @@ -43,7 +43,7 @@ import * as _blockedRequests from "./blockedRequests.js" import * as internalCause from "./cause.js" import * as deferred from "./deferred.js" import * as internalDiffer from "./differ.js" -import { effectVariance, StructuralCommitPrototype } from "./effectable.js" +import { CommitPrototype, effectVariance, StructuralCommitPrototype } from "./effectable.js" import { getBugErrorMessage } from "./errors.js" import type * as FiberRuntime from "./fiberRuntime.js" import type * as fiberScope from "./fiberScope.js" @@ -2787,14 +2787,18 @@ const exitCollectAllInternal = ( // ----------------------------------------------------------------------------- /** @internal */ -export const deferredUnsafeMake = (fiberId: FiberId.FiberId): Deferred.Deferred => ({ - [deferred.DeferredTypeId]: deferred.deferredVariance, - state: MutableRef.make(deferred.pending([])), - blockingOn: fiberId, - pipe() { - return pipeArguments(this, arguments) +export const deferredUnsafeMake = (fiberId: FiberId.FiberId): Deferred.Deferred => { + const _deferred = { + ...CommitPrototype, + [deferred.DeferredTypeId]: deferred.deferredVariance, + state: MutableRef.make(deferred.pending([])), + commit() { + return deferredAwait(this) + }, + blockingOn: fiberId } -}) + return _deferred +} /* @internal */ export const deferredMake = (): Effect.Effect> => diff --git a/packages/effect/test/Deferred.test.ts b/packages/effect/test/Deferred.test.ts index f1b6ffc793..2862b4afb2 100644 --- a/packages/effect/test/Deferred.test.ts +++ b/packages/effect/test/Deferred.test.ts @@ -157,4 +157,14 @@ describe("Deferred", () => { ) assert.isTrue(Exit.isInterrupted(result)) })) + it.effect("is subtype of Effect", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const ref = yield* Ref.make(13) + yield* Deferred.complete(deferred, Ref.updateAndGet(ref, (n) => n + 1)) + const result1 = yield* deferred + const result2 = yield* deferred + assert.strictEqual(result1, 14) + assert.strictEqual(result2, 14) + })) }) From 74c2a63e3966789f4fc179208a9160ae953ab1c6 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 9 Sep 2024 04:38:04 +0300 Subject: [PATCH 08/22] add Stream.share api (#3080) Co-authored-by: Tim --- .changeset/stream-share.md | 10 ++ packages/effect/src/Stream.ts | 36 +++++++ packages/effect/src/internal/stream.ts | 53 +++++++++++ .../effect/test/Stream/broadcasting.test.ts | 94 +++++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 .changeset/stream-share.md diff --git a/.changeset/stream-share.md b/.changeset/stream-share.md new file mode 100644 index 0000000000..f1dee66bd5 --- /dev/null +++ b/.changeset/stream-share.md @@ -0,0 +1,10 @@ +--- +"effect": minor +--- + +add `Stream.share` api + +The `Stream.share` api is a ref counted variant of the broadcast apis. + +It allows you to share a stream between multiple consumers, and will close the +upstream when the last consumer ends. diff --git a/packages/effect/src/Stream.ts b/packages/effect/src/Stream.ts index 532668cab9..adbc0c9242 100644 --- a/packages/effect/src/Stream.ts +++ b/packages/effect/src/Stream.ts @@ -549,6 +549,42 @@ export const broadcast: { ): Effect.Effect>, never, Scope.Scope | R> } = internal.broadcast +/** + * Returns a new Stream that multicasts the original Stream, subscribing to it as soon as the first consumer subscribes. + * As long as there is at least one consumer, the upstream will continue running and emitting data. + * When all consumers have exited, the upstream will be finalized. + * + * @since 3.8.0 + * @category utils + */ +export const share: { + ( + config: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } + ): (self: Stream) => Effect.Effect, never, R | Scope.Scope> + ( + self: Stream, + config: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } + ): Effect.Effect, never, R | Scope.Scope> +} = internal.share + /** * Fan out the stream, producing a dynamic number of streams that have the * same elements as this stream. The driver stream will only ever advance the diff --git a/packages/effect/src/internal/stream.ts b/packages/effect/src/internal/stream.ts index 1524d0e221..3a4b6d0bb9 100644 --- a/packages/effect/src/internal/stream.ts +++ b/packages/effect/src/internal/stream.ts @@ -21,6 +21,7 @@ import { pipeArguments } from "../Pipeable.js" import { hasProperty, isTagged, type Predicate, type Refinement } from "../Predicate.js" import * as PubSub from "../PubSub.js" import * as Queue from "../Queue.js" +import * as RcRef from "../RcRef.js" import * as Ref from "../Ref.js" import * as Runtime from "../Runtime.js" import * as Schedule from "../Schedule.js" @@ -837,6 +838,58 @@ export const broadcastDynamic = dual< ): Effect.Effect, never, Scope.Scope | R> => Effect.map(toPubSub(self, maximumLag), (pubsub) => flattenTake(fromPubSub(pubsub)))) +export const share = dual< + ( + config: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } + ) => ( + self: Stream.Stream + ) => Effect.Effect, never, R | Scope.Scope>, + ( + self: Stream.Stream, + config: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } + ) => Effect.Effect, never, R | Scope.Scope> +>( + 2, + ( + self: Stream.Stream, + options: { + readonly capacity: "unbounded" + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + readonly idleTimeToLive?: Duration.DurationInput | undefined + } + ): Effect.Effect, never, R | Scope.Scope> => + Effect.map( + RcRef.make({ + acquire: broadcastDynamic(self, options), + idleTimeToLive: options.idleTimeToLive + }), + (rcRef) => unwrapScoped(RcRef.get(rcRef)) + ) +) + /** @internal */ export const broadcastedQueues = dual< ( diff --git a/packages/effect/test/Stream/broadcasting.test.ts b/packages/effect/test/Stream/broadcasting.test.ts index b9fedaed0e..e83cb7b220 100644 --- a/packages/effect/test/Stream/broadcasting.test.ts +++ b/packages/effect/test/Stream/broadcasting.test.ts @@ -5,8 +5,11 @@ import * as Either from "effect/Either" import * as Fiber from "effect/Fiber" import { pipe } from "effect/Function" import * as Ref from "effect/Ref" +import * as Schedule from "effect/Schedule" +import * as Sink from "effect/Sink" import * as Stream from "effect/Stream" import * as it from "effect/test/utils/extend" +import * as TestClock from "effect/TestClock" import { assert, describe } from "vitest" describe("Stream", () => { @@ -101,4 +104,95 @@ describe("Stream", () => { ) assert.deepStrictEqual(Array.from(result), [0, 1, 2, 3, 4]) })) + + it.scoped("share sequenced", () => + Effect.gen(function*() { + const sharedStream = yield* Stream.fromSchedule(Schedule.spaced("1 seconds")).pipe( + Stream.share({ capacity: 16 }) + ) + + const firstFiber = yield* sharedStream.pipe( + Stream.take(1), + Stream.run(Sink.collectAll()), + Effect.map(Array.from), + Effect.fork + ) + + yield* TestClock.adjust("1 second") + + const first = yield* Fiber.join(firstFiber) + assert.deepStrictEqual(first, [0]) + + const secondFiber = yield* sharedStream.pipe( + Stream.take(1), + Stream.run(Sink.collectAll()), + Effect.map(Array.from), + Effect.fork + ) + + yield* TestClock.adjust("1 second") + + const second = yield* Fiber.join(secondFiber) + assert.deepStrictEqual(second, [0]) + })) + + it.scoped("share sequenced with idleTimeToLive", () => + Effect.gen(function*() { + const sharedStream = yield* Stream.fromSchedule(Schedule.spaced("1 seconds")).pipe( + Stream.share({ + capacity: 16, + idleTimeToLive: "1 second" + }) + ) + + const firstFiber = yield* sharedStream.pipe( + Stream.take(1), + Stream.run(Sink.collectAll()), + Effect.map(Array.from), + Effect.fork + ) + + yield* TestClock.adjust("1 second") + + const first = yield* Fiber.join(firstFiber) + assert.deepStrictEqual(first, [0]) + + const secondFiber = yield* sharedStream.pipe( + Stream.take(1), + Stream.run(Sink.collectAll()), + Effect.map(Array.from), + Effect.fork + ) + + yield* TestClock.adjust("1 second") + + const second = yield* Fiber.join(secondFiber) + assert.deepStrictEqual(second, [1]) + })) + + it.scoped("share parallel", () => + Effect.gen(function*() { + const sharedStream = yield* Stream.fromSchedule(Schedule.spaced("1 seconds")).pipe( + Stream.share({ capacity: 16 }) + ) + + const fiber1 = yield* sharedStream.pipe( + Stream.take(1), + Stream.run(Sink.collectAll()), + Effect.map((x) => Array.from(x)), + Effect.fork + ) + const fiber2 = yield* sharedStream.pipe( + Stream.take(2), + Stream.run(Sink.collectAll()), + Effect.map((x) => Array.from(x)), + Effect.fork + ) + + yield* TestClock.adjust("2 second") + const [result1, result2] = yield* Fiber.joinAll([fiber1, fiber2]) + + assert.deepStrictEqual(result1, [0]) + assert.deepStrictEqual(result2, [0, 1]) + })) }) From 9303a0943ef9e7da2fadc143d9af609dafa8bf9e Mon Sep 17 00:00:00 2001 From: Maxim Khramtsov Date: Tue, 10 Sep 2024 02:27:59 +0200 Subject: [PATCH 09/22] `FiberRef` subtype of `Effect` (#3577) Co-authored-by: maksim.khramtsov --- .changeset/wise-kiwis-tan.md | 16 ++++++++++++++++ packages/effect/dtslint/Unify.ts | 10 +++++++++- packages/effect/src/FiberRef.ts | 23 +++++++++++++++++++++-- packages/effect/src/internal/core.ts | 26 +++++++++++++++----------- packages/effect/test/FiberRef.test.ts | 6 ++++++ 5 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 .changeset/wise-kiwis-tan.md diff --git a/.changeset/wise-kiwis-tan.md b/.changeset/wise-kiwis-tan.md new file mode 100644 index 0000000000..e0dd75dca8 --- /dev/null +++ b/.changeset/wise-kiwis-tan.md @@ -0,0 +1,16 @@ +--- +"effect": minor +--- + +The `FiberRef` is now a subtype of `Effect`. This change simplifies handling of deferred values, removing the need for explicit call `FiberRef.get`. + +```typescript +import { Effect, FiberRef } from "effect" + +Effect.gen(function* () { + const fiberRef = yield* FiberRef.make("value") + + const before = yield* FiberRef.get(fiberRef) + const after = yield* fiberRef +}) +``` diff --git a/packages/effect/dtslint/Unify.ts b/packages/effect/dtslint/Unify.ts index 87baa5d34e..175fe08137 100644 --- a/packages/effect/dtslint/Unify.ts +++ b/packages/effect/dtslint/Unify.ts @@ -2,6 +2,7 @@ import type * as Deferred from "effect/Deferred" import type * as Effect from "effect/Effect" import * as Either from "effect/Either" import type * as Exit from "effect/Exit" +import type * as FiberRef from "effect/FiberRef" import type * as Micro from "effect/Micro" import type * as Option from "effect/Option" import type * as RcRef from "effect/RcRef" @@ -72,8 +73,13 @@ export type DeferredUnify = Unify.Unify< | Deferred.Deferred<1, 2> | Deferred.Deferred<"a", "b"> > +// $ExpectType FiberRef<1> | FiberRef<"a"> +export type FiberRefUnify = Unify.Unify< + | FiberRef.FiberRef<1> + | FiberRef.FiberRef<"a"> +> -// $ExpectType 0 | Option | Ref<1> | SynchronizedRef<1> | SubscriptionRef<1> | Deferred<1, 2> | Deferred<"a", "b"> | Ref<"A"> | SynchronizedRef<"A"> | SubscriptionRef<"A"> | Either<1 | "A", 0 | "E"> | Effect<1 | "A", 0 | "E", "R" | "R1"> | RcRef<1 | "A", 0 | "E"> +// $ExpectType 0 | Option | Ref<1> | SynchronizedRef<1> | SubscriptionRef<1> | Deferred<1, 2> | Deferred<"a", "b"> | Ref<"A"> | SynchronizedRef<"A"> | SubscriptionRef<"A"> | FiberRef<12> | FiberRef<"a2"> | Either<1 | "A", 0 | "E"> | Effect<1 | "A", 0 | "E", "R" | "R1"> | RcRef<1 | "A", 0 | "E"> export type AllUnify = Unify.Unify< | Either.Either<1, 0> | Either.Either<"A", "E"> @@ -91,5 +97,7 @@ export type AllUnify = Unify.Unify< | RcRef.RcRef<"A", "E"> | Deferred.Deferred<1, 2> | Deferred.Deferred<"a", "b"> + | FiberRef.FiberRef<12> + | FiberRef.FiberRef<"a2"> | 0 > diff --git a/packages/effect/src/FiberRef.ts b/packages/effect/src/FiberRef.ts index 463858adbc..ae95e146b0 100644 --- a/packages/effect/src/FiberRef.ts +++ b/packages/effect/src/FiberRef.ts @@ -18,7 +18,6 @@ import type * as LogLevel from "./LogLevel.js" import type * as LogSpan from "./LogSpan.js" import type * as MetricLabel from "./MetricLabel.js" import type * as Option from "./Option.js" -import type { Pipeable } from "./Pipeable.js" import type * as Request from "./Request.js" import type * as RuntimeFlags from "./RuntimeFlags.js" import * as Scheduler from "./Scheduler.js" @@ -26,6 +25,7 @@ import type * as Scope from "./Scope.js" import type * as Supervisor from "./Supervisor.js" import type * as Tracer from "./Tracer.js" import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" /** * @since 2.0.0 @@ -43,7 +43,7 @@ export type FiberRefTypeId = typeof FiberRefTypeId * @since 2.0.0 * @category model */ -export interface FiberRef extends Variance, Pipeable { +export interface FiberRef extends Effect.Effect, Variance { /** @internal */ readonly initial: A /** @internal */ @@ -56,6 +56,25 @@ export interface FiberRef extends Variance, Pipeable { readonly fork: unknown /** @internal */ join(oldValue: A, newValue: A): A + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: FiberRefUnify + readonly [Unify.ignoreSymbol]?: FiberRefUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface FiberRefUnify extends Effect.EffectUnify { + FiberRef?: () => Extract> +} + +/** + * @category models + * @since 3.8.0 + */ +export interface FiberRefUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true } /** diff --git a/packages/effect/src/internal/core.ts b/packages/effect/src/internal/core.ts index 8216dfcd93..4b3350fba3 100644 --- a/packages/effect/src/internal/core.ts +++ b/packages/effect/src/internal/core.ts @@ -1955,18 +1955,22 @@ export const fiberRefUnsafeMakePatch = ( readonly fork: Patch readonly join?: ((oldV: Value, newV: Value) => Value) | undefined } -): FiberRef.FiberRef => ({ - [FiberRefTypeId]: fiberRefVariance, - initial, - diff: (oldValue, newValue) => options.differ.diff(oldValue, newValue), - combine: (first, second) => options.differ.combine(first as Patch, second as Patch), - patch: (patch) => (oldValue) => options.differ.patch(patch as Patch, oldValue), - fork: options.fork, - join: options.join ?? ((_, n) => n), - pipe() { - return pipeArguments(this, arguments) +): FiberRef.FiberRef => { + const _fiberRef = { + ...CommitPrototype, + [FiberRefTypeId]: fiberRefVariance, + initial, + commit() { + return fiberRefGet(this) + }, + diff: (oldValue: Value, newValue: Value) => options.differ.diff(oldValue, newValue), + combine: (first: Patch, second: Patch) => options.differ.combine(first, second), + patch: (patch: Patch) => (oldValue: Value) => options.differ.patch(patch, oldValue), + fork: options.fork, + join: options.join ?? ((_, n) => n) } -}) + return _fiberRef +} /** @internal */ export const fiberRefUnsafeMakeRuntimeFlags = ( diff --git a/packages/effect/test/FiberRef.test.ts b/packages/effect/test/FiberRef.test.ts index 9f8e928d13..41534de743 100644 --- a/packages/effect/test/FiberRef.test.ts +++ b/packages/effect/test/FiberRef.test.ts @@ -361,4 +361,10 @@ describe("FiberRef", () => { const result = yield* $(Deferred.await(deferred)) assert.isTrue(result) })) + it.scoped("is subtype of Effect", () => + Effect.gen(function*() { + const fiberRef = yield* FiberRef.make(initial) + const result = yield* fiberRef + assert.strictEqual(result, initial) + })) }) From d2e5807189f17eaaa91c83683e0a603b7ee06a02 Mon Sep 17 00:00:00 2001 From: Maxim Khramtsov Date: Thu, 12 Sep 2024 01:48:01 +0200 Subject: [PATCH 10/22] make Fiber subtype of Effect (#3590) Co-authored-by: maksim.khramtsov --- .changeset/seven-lamps-nail.md | 16 ++++ packages/effect/dtslint/Unify.ts | 17 +++- packages/effect/src/Fiber.ts | 45 ++++++++- .../effect/src/internal/effect/circular.ts | 4 + packages/effect/src/internal/fiber.ts | 85 +++++++++++------ packages/effect/src/internal/fiberRuntime.ts | 95 ++++++++++--------- packages/effect/test/Fiber.test.ts | 6 ++ 7 files changed, 192 insertions(+), 76 deletions(-) create mode 100644 .changeset/seven-lamps-nail.md diff --git a/.changeset/seven-lamps-nail.md b/.changeset/seven-lamps-nail.md new file mode 100644 index 0000000000..7691b1b0f9 --- /dev/null +++ b/.changeset/seven-lamps-nail.md @@ -0,0 +1,16 @@ +--- +"effect": minor +--- + +The `Fiber` is now a subtype of `Effect`. This change removes the need for explicit call `Fiber.join`. + +```typescript +import { Effect, Fiber } from "effect" + +Effect.gen(function*() { + const fiber = yield* Effect.fork(Effect.succeed(1)) + + const oldWay = yield* Fiber.join(fiber) + const now = yield* fiber +})) +``` diff --git a/packages/effect/dtslint/Unify.ts b/packages/effect/dtslint/Unify.ts index 175fe08137..835d43df22 100644 --- a/packages/effect/dtslint/Unify.ts +++ b/packages/effect/dtslint/Unify.ts @@ -2,6 +2,7 @@ import type * as Deferred from "effect/Deferred" import type * as Effect from "effect/Effect" import * as Either from "effect/Either" import type * as Exit from "effect/Exit" +import type * as Fiber from "effect/Fiber" import type * as FiberRef from "effect/FiberRef" import type * as Micro from "effect/Micro" import type * as Option from "effect/Option" @@ -78,8 +79,18 @@ export type FiberRefUnify = Unify.Unify< | FiberRef.FiberRef<1> | FiberRef.FiberRef<"a"> > +// $ExpectType Fiber<"a" | 1, "b" | 2> +export type FiberUnify = Unify.Unify< + | Fiber.Fiber<1, 2> + | Fiber.Fiber<"a", "b"> +> +// $ExpectType RuntimeFiber<"a" | 1, "b" | 2> +export type RuntimeFiberUnify = Unify.Unify< + | Fiber.RuntimeFiber<1, 2> + | Fiber.RuntimeFiber<"a", "b"> +> -// $ExpectType 0 | Option | Ref<1> | SynchronizedRef<1> | SubscriptionRef<1> | Deferred<1, 2> | Deferred<"a", "b"> | Ref<"A"> | SynchronizedRef<"A"> | SubscriptionRef<"A"> | FiberRef<12> | FiberRef<"a2"> | Either<1 | "A", 0 | "E"> | Effect<1 | "A", 0 | "E", "R" | "R1"> | RcRef<1 | "A", 0 | "E"> +// $ExpectType 0 | Option | Ref<1> | SynchronizedRef<1> | SubscriptionRef<1> | Deferred<1, 2> | Deferred<"a", "b"> | Fiber<"a" | 1, "b" | 2> | RuntimeFiber<"a" | 1, "b" | 2> | Ref<"A"> | SynchronizedRef<"A"> | SubscriptionRef<"A"> | FiberRef<12> | FiberRef<"a2"> | Either<1 | "A", 0 | "E"> | Effect<1 | "A", 0 | "E", "R" | "R1"> | RcRef<1 | "A", 0 | "E"> export type AllUnify = Unify.Unify< | Either.Either<1, 0> | Either.Either<"A", "E"> @@ -99,5 +110,9 @@ export type AllUnify = Unify.Unify< | Deferred.Deferred<"a", "b"> | FiberRef.FiberRef<12> | FiberRef.FiberRef<"a2"> + | Fiber.Fiber<1, 2> + | Fiber.Fiber<"a", "b"> + | Fiber.RuntimeFiber<1, 2> + | Fiber.RuntimeFiber<"a", "b"> | 0 > diff --git a/packages/effect/src/Fiber.ts b/packages/effect/src/Fiber.ts index 3f18046372..3c3e76065b 100644 --- a/packages/effect/src/Fiber.ts +++ b/packages/effect/src/Fiber.ts @@ -18,13 +18,13 @@ import * as internal from "./internal/fiber.js" import * as fiberRuntime from "./internal/fiberRuntime.js" import type * as Option from "./Option.js" import type * as order from "./Order.js" -import type { Pipeable } from "./Pipeable.js" import type * as RuntimeFlags from "./RuntimeFlags.js" import type { Scheduler } from "./Scheduler.js" import type * as Scope from "./Scope.js" import type { Supervisor } from "./Supervisor.js" import type { AnySpan, Tracer } from "./Tracer.js" import type * as Types from "./Types.js" +import type * as Unify from "./Unify.js" /** * @since 2.0.0 @@ -62,7 +62,7 @@ export type RuntimeFiberTypeId = typeof RuntimeFiberTypeId * @since 2.0.0 * @category models */ -export interface Fiber extends Fiber.Variance, Pipeable { +export interface Fiber extends Effect.Effect, Fiber.Variance { /** * The identity of the fiber. */ @@ -97,6 +97,26 @@ export interface Fiber extends Fiber.Variance, Pipea * resume immediately. Otherwise, the effect will resume when the fiber exits. */ interruptAsFork(fiberId: FiberId.FiberId): Effect.Effect + + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: FiberUnify + readonly [Unify.ignoreSymbol]?: FiberUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface FiberUnify extends Effect.EffectUnify { + Fiber?: () => A[Unify.typeSymbol] extends Fiber | infer _ ? Fiber : never +} + +/** + * @category models + * @since 3.8.0 + */ +export interface FiberUnifyIgnore extends Effect.EffectUnifyIgnore { + Effect?: true } /** @@ -190,6 +210,27 @@ export interface RuntimeFiber extends Fiber, Fiber.R * Gets the current supervisor */ get currentSupervisor(): Supervisor + + readonly [Unify.typeSymbol]?: unknown + readonly [Unify.unifySymbol]?: RuntimeFiberUnify + readonly [Unify.ignoreSymbol]?: RuntimeFiberUnifyIgnore +} + +/** + * @category models + * @since 3.8.0 + */ +export interface RuntimeFiberUnify extends FiberUnify { + RuntimeFiber?: () => A[Unify.typeSymbol] extends RuntimeFiber | infer _ ? RuntimeFiber + : never +} + +/** + * @category models + * @since 3.8.0 + */ +export interface RuntimeFiberUnifyIgnore extends FiberUnifyIgnore { + Fiber?: true } /** diff --git a/packages/effect/src/internal/effect/circular.ts b/packages/effect/src/internal/effect/circular.ts index da384e5457..cff08b7670 100644 --- a/packages/effect/src/internal/effect/circular.ts +++ b/packages/effect/src/internal/effect/circular.ts @@ -665,6 +665,10 @@ export const zipWithFiber = dual< f: (a: A, b: B) => C ) => Fiber.Fiber >(3, (self, that, f) => ({ + ...Effectable.CommitPrototype, + commit() { + return internalFiber.join(this) + }, [internalFiber.FiberTypeId]: internalFiber.fiberVariance, id: () => pipe(self.id(), FiberId.getOrElse(that.id())), await: pipe( diff --git a/packages/effect/src/internal/fiber.ts b/packages/effect/src/internal/fiber.ts index 5f4e1a88a2..dae7a6f1ac 100644 --- a/packages/effect/src/internal/fiber.ts +++ b/packages/effect/src/internal/fiber.ts @@ -14,6 +14,7 @@ import * as order from "../Order.js" import { pipeArguments } from "../Pipeable.js" import { hasProperty } from "../Predicate.js" import * as core from "./core.js" +import * as effectable from "./effectable.js" import * as fiberScope from "./fiberScope.js" import * as runtimeFlags from "./runtimeFlags.js" @@ -76,15 +77,23 @@ export const children = ( ): Effect.Effect>> => self.children /** @internal */ -export const done = (exit: Exit.Exit): Fiber.Fiber => ({ - ...fiberProto, - id: () => FiberId.none, - await: core.succeed(exit), - children: core.succeed([]), - inheritAll: core.void, - poll: core.succeed(Option.some(exit)), - interruptAsFork: () => core.void -}) +export const done = (exit: Exit.Exit): Fiber.Fiber => { + const _fiber = { + ...effectable.CommitPrototype, + commit() { + return join(this) + }, + ...fiberProto, + id: () => FiberId.none, + await: core.succeed(exit), + children: core.succeed([]), + inheritAll: core.void, + poll: core.succeed(Option.some(exit)), + interruptAsFork: () => core.void + } + + return _fiber +} /** @internal */ export const dump = (self: Fiber.RuntimeFiber): Effect.Effect => @@ -148,25 +157,32 @@ export const map = dual< export const mapEffect = dual< (f: (a: A) => Effect.Effect) => (self: Fiber.Fiber) => Fiber.Fiber, (self: Fiber.Fiber, f: (a: A) => Effect.Effect) => Fiber.Fiber ->(2, (self, f) => ({ - ...fiberProto, - id: () => self.id(), - await: core.flatMap(self.await, Exit.forEachEffect(f)), - children: self.children, - inheritAll: self.inheritAll, - poll: core.flatMap(self.poll, (result) => { - switch (result._tag) { - case "None": - return core.succeed(Option.none()) - case "Some": - return pipe( - Exit.forEachEffect(result.value, f), - core.map(Option.some) - ) - } - }), - interruptAsFork: (id) => self.interruptAsFork(id) -})) +>(2, (self, f) => { + const _fiber = { + ...effectable.CommitPrototype, + commit() { + return join(this) + }, + ...fiberProto, + id: () => self.id(), + await: core.flatMap(self.await, Exit.forEachEffect(f)), + children: self.children, + inheritAll: self.inheritAll, + poll: core.flatMap(self.poll, (result) => { + switch (result._tag) { + case "None": + return core.succeed(Option.none()) + case "Some": + return pipe( + Exit.forEachEffect(result.value, f), + core.map(Option.some) + ) + } + }), + interruptAsFork: (id: FiberId.FiberId) => self.interruptAsFork(id) + } + return _fiber +}) /** @internal */ export const mapFiber = dual< @@ -212,7 +228,11 @@ export const match = dual< }) /** @internal */ -export const never: Fiber.Fiber = { +const _never = { + ...effectable.CommitPrototype, + commit() { + return join(this) + }, ...fiberProto, id: () => FiberId.none, await: core.never, @@ -222,11 +242,18 @@ export const never: Fiber.Fiber = { interruptAsFork: () => core.never } +/** @internal */ +export const never: Fiber.Fiber = _never + /** @internal */ export const orElse = dual< (that: Fiber.Fiber) => (self: Fiber.Fiber) => Fiber.Fiber, (self: Fiber.Fiber, that: Fiber.Fiber) => Fiber.Fiber >(2, (self, that) => ({ + ...effectable.CommitPrototype, + commit() { + return join(this) + }, ...fiberProto, id: () => FiberId.getOrElse(self.id(), that.id()), await: core.zipWith( diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index e2cac6688d..f75f7d3e80 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -10,7 +10,7 @@ import type { DefaultServices } from "../DefaultServices.js" import * as Deferred from "../Deferred.js" import type * as Duration from "../Duration.js" import type * as Effect from "../Effect.js" -import { EffectTypeId } from "../Effectable.js" +import * as Effectable from "../Effectable.js" import type * as Either from "../Either.js" import * as ExecutionStrategy from "../ExecutionStrategy.js" import type * as Exit from "../Exit.js" @@ -269,14 +269,11 @@ export interface Snapshot { } /** @internal */ -export class FiberRuntime implements Fiber.RuntimeFiber { +export class FiberRuntime extends Effectable.Class + implements Fiber.RuntimeFiber +{ readonly [internalFiber.FiberTypeId] = internalFiber.fiberVariance readonly [internalFiber.RuntimeFiberTypeId] = runtimeFiberVariance - - pipe() { - return pipeArguments(this, arguments) - } - private _fiberRefs: FiberRefs.FiberRefs private _fiberId: FiberId.Runtime private _queue = new Array() @@ -304,6 +301,7 @@ export class FiberRuntime implements Fiber.RuntimeFi fiberRefs0: FiberRefs.FiberRefs, runtimeFlags0: RuntimeFlags.RuntimeFlags ) { + super() this.currentRuntimeFlags = runtimeFlags0 this._fiberId = fiberId this._fiberRefs = fiberRefs0 @@ -315,6 +313,10 @@ export class FiberRuntime implements Fiber.RuntimeFi this.refreshRefCache() } + commit(): Effect.Effect { + return internalFiber.join(this) + } + /** * The identity of the fiber. */ @@ -1334,10 +1336,10 @@ export class FiberRuntime implements Fiber.RuntimeFi // @ts-expect-error cur = this.currentTracer.context( () => { - if (version.getCurrentVersion() !== (cur as core.Primitive)[EffectTypeId]._V) { + if (version.getCurrentVersion() !== (cur as core.Primitive)[core.EffectTypeId]._V) { return core.dieMessage( `Cannot execute an Effect versioned ${ - (cur as core.Primitive)[EffectTypeId]._V + (cur as core.Primitive)[core.EffectTypeId]._V } with a Runtime of version ${version.getCurrentVersion()}` ) } @@ -3365,46 +3367,51 @@ export const fiberAwaitAll = >>( > => forEach(fibers, internalFiber._await) as any /** @internal */ -export const fiberAll = (fibers: Iterable>): Fiber.Fiber, E> => ({ - [internalFiber.FiberTypeId]: internalFiber.fiberVariance, - id: () => - RA.fromIterable(fibers).reduce((id, fiber) => FiberId.combine(id, fiber.id()), FiberId.none as FiberId.FiberId), - await: core.exit(forEachParUnbounded(fibers, (fiber) => core.flatten(fiber.await), false)), - children: core.map(forEachParUnbounded(fibers, (fiber) => fiber.children, false), RA.flatten), - inheritAll: core.forEachSequentialDiscard(fibers, (fiber) => fiber.inheritAll), - poll: core.map( - core.forEachSequential(fibers, (fiber) => fiber.poll), - RA.reduceRight( - Option.some, E>>(core.exitSucceed(new Array())), - (optionB, optionA) => { - switch (optionA._tag) { - case "None": { - return Option.none() - } - case "Some": { - switch (optionB._tag) { - case "None": { - return Option.none() - } - case "Some": { - return Option.some( - core.exitZipWith(optionA.value, optionB.value, { - onSuccess: (a, chunk) => [a, ...chunk], - onFailure: internalCause.parallel - }) - ) +export const fiberAll = (fibers: Iterable>): Fiber.Fiber, E> => { + const _fiberAll = { + ...Effectable.CommitPrototype, + commit() { + return internalFiber.join(this) + }, + [internalFiber.FiberTypeId]: internalFiber.fiberVariance, + id: () => + RA.fromIterable(fibers).reduce((id, fiber) => FiberId.combine(id, fiber.id()), FiberId.none as FiberId.FiberId), + await: core.exit(forEachParUnbounded(fibers, (fiber) => core.flatten(fiber.await), false)), + children: core.map(forEachParUnbounded(fibers, (fiber) => fiber.children, false), RA.flatten), + inheritAll: core.forEachSequentialDiscard(fibers, (fiber) => fiber.inheritAll), + poll: core.map( + core.forEachSequential(fibers, (fiber) => fiber.poll), + RA.reduceRight( + Option.some, E>>(core.exitSucceed(new Array())), + (optionB, optionA) => { + switch (optionA._tag) { + case "None": { + return Option.none() + } + case "Some": { + switch (optionB._tag) { + case "None": { + return Option.none() + } + case "Some": { + return Option.some( + core.exitZipWith(optionA.value, optionB.value, { + onSuccess: (a, chunk) => [a, ...chunk], + onFailure: internalCause.parallel + }) + ) + } } } } } - } - ) - ), - interruptAsFork: (fiberId) => core.forEachSequentialDiscard(fibers, (fiber) => fiber.interruptAsFork(fiberId)), - pipe() { - return pipeArguments(this, arguments) + ) + ), + interruptAsFork: (fiberId: FiberId.FiberId) => + core.forEachSequentialDiscard(fibers, (fiber) => fiber.interruptAsFork(fiberId)) } -}) + return _fiberAll +} /* @internal */ export const fiberInterruptFork = (self: Fiber.Fiber): Effect.Effect => diff --git a/packages/effect/test/Fiber.test.ts b/packages/effect/test/Fiber.test.ts index 1bd813ccae..0442f7b44f 100644 --- a/packages/effect/test/Fiber.test.ts +++ b/packages/effect/test/Fiber.test.ts @@ -226,4 +226,10 @@ describe("Fiber", () => { const result = yield* $(Fiber.join(Fiber.all(fibers)), Effect.asVoid) assert.isUndefined(result) }), 10000) + it.effect("is subtype of Effect", () => + Effect.gen(function*() { + const fiber = yield* Effect.fork(Effect.succeed(1)) + const fiberResult = yield* fiber + assert(1 === fiberResult) + })) }) From 21b89163656224f79374d47c45bb71550f23494c Mon Sep 17 00:00:00 2001 From: Sebastian Lorenz Date: Thu, 12 Sep 2024 22:56:50 +0200 Subject: [PATCH 11/22] add filter refinement overloads to http client (#3595) --- .changeset/thick-coats-buy.md | 5 +++ packages/platform/src/HttpClient.ts | 38 ++++++++++++---- packages/platform/src/internal/httpClient.ts | 46 ++++++++++++++------ 3 files changed, 66 insertions(+), 23 deletions(-) create mode 100644 .changeset/thick-coats-buy.md diff --git a/.changeset/thick-coats-buy.md b/.changeset/thick-coats-buy.md new file mode 100644 index 0000000000..8cc5fdb2e0 --- /dev/null +++ b/.changeset/thick-coats-buy.md @@ -0,0 +1,5 @@ +--- +"@effect/platform": patch +--- + +Added refinement overloads to `HttpClient.filterOrFail` and `HttpClient.filterOrElse` diff --git a/packages/platform/src/HttpClient.ts b/packages/platform/src/HttpClient.ts index 45f8b3d819..d89b55738e 100644 --- a/packages/platform/src/HttpClient.ts +++ b/packages/platform/src/HttpClient.ts @@ -174,15 +174,26 @@ export const catchTags: { * @category filters */ export const filterOrElse: { + ( + refinement: Predicate.Refinement, B>, + orElse: (a: NoInfer) => Effect.Effect + ): (self: HttpClient) => HttpClient ( - f: Predicate.Predicate, - orElse: (a: A) => Effect.Effect - ): (self: HttpClient) => HttpClient + predicate: Predicate.Predicate>, + orElse: (a: NoInfer) => Effect.Effect + ): ( + self: HttpClient + ) => HttpClient + ( + self: HttpClient, + refinement: Predicate.Refinement, + orElse: (a: A) => Effect.Effect + ): HttpClient ( self: HttpClient, - f: Predicate.Predicate, + predicate: Predicate.Predicate, orElse: (a: A) => Effect.Effect - ): HttpClient + ): HttpClient } = internal.filterOrElse /** @@ -190,15 +201,24 @@ export const filterOrElse: { * @category filters */ export const filterOrFail: { + ( + refinement: Predicate.Refinement, B>, + orFailWith: (a: NoInfer) => E2 + ): (self: HttpClient) => HttpClient ( - f: Predicate.Predicate, - orFailWith: (a: A) => E2 + predicate: Predicate.Predicate>, + orFailWith: (a: NoInfer) => E2 ): (self: HttpClient) => HttpClient + ( + self: HttpClient, + refinement: Predicate.Refinement, + orFailWith: (a: A) => E2 + ): HttpClient ( self: HttpClient, - f: Predicate.Predicate, + predicate: Predicate.Predicate, orFailWith: (a: A) => E2 - ): HttpClient + ): HttpClient } = internal.filterOrFail /** diff --git a/packages/platform/src/internal/httpClient.ts b/packages/platform/src/internal/httpClient.ts index 8fc588308f..3398146422 100644 --- a/packages/platform/src/internal/httpClient.ts +++ b/packages/platform/src/internal/httpClient.ts @@ -476,32 +476,50 @@ export const catchAll: { ) /** @internal */ -export const filterOrElse = dual< +export const filterOrElse: { + ( + refinement: Predicate.Refinement, B>, + orElse: (a: NoInfer) => Effect.Effect + ): (self: Client.HttpClient) => Client.HttpClient ( - f: Predicate.Predicate, - orElse: (a: A) => Effect.Effect - ) => ( + predicate: Predicate.Predicate>, + orElse: (a: NoInfer) => Effect.Effect + ): ( self: Client.HttpClient - ) => Client.HttpClient, + ) => Client.HttpClient + ( + self: Client.HttpClient, + refinement: Predicate.Refinement, + orElse: (a: A) => Effect.Effect + ): Client.HttpClient ( self: Client.HttpClient, - f: Predicate.Predicate, + predicate: Predicate.Predicate, orElse: (a: A) => Effect.Effect - ) => Client.HttpClient ->(3, (self, f, orElse) => transformResponse(self, Effect.filterOrElse(f, orElse))) + ): Client.HttpClient +} = dual(3, (self, f, orElse) => transformResponse(self, Effect.filterOrElse(f, orElse))) /** @internal */ -export const filterOrFail = dual< +export const filterOrFail: { + ( + refinement: Predicate.Refinement, B>, + orFailWith: (a: NoInfer) => E2 + ): (self: Client.HttpClient) => Client.HttpClient ( - f: Predicate.Predicate, + predicate: Predicate.Predicate>, + orFailWith: (a: NoInfer) => E2 + ): (self: Client.HttpClient) => Client.HttpClient + ( + self: Client.HttpClient, + refinement: Predicate.Refinement, orFailWith: (a: A) => E2 - ) => (self: Client.HttpClient) => Client.HttpClient, + ): Client.HttpClient ( self: Client.HttpClient, - f: Predicate.Predicate, + predicate: Predicate.Predicate, orFailWith: (a: A) => E2 - ) => Client.HttpClient ->(3, (self, f, orFailWith) => transformResponse(self, Effect.filterOrFail(f, orFailWith))) + ): Client.HttpClient +} = dual(3, (self, f, orFailWith) => transformResponse(self, Effect.filterOrFail(f, orFailWith))) /** @internal */ export const map = dual< From 1c53531517567c8f1cc6b34d5206561d5c47684f Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 13 Sep 2024 09:57:53 +1200 Subject: [PATCH 12/22] add Semaphore.withPermitsIfAvailable (#3593) --- .changeset/few-mayflies-speak.md | 21 +++++++++++++++++++ packages/effect/src/Effect.ts | 2 ++ .../effect/src/internal/effect/circular.ts | 11 ++++++++++ 3 files changed, 34 insertions(+) create mode 100644 .changeset/few-mayflies-speak.md diff --git a/.changeset/few-mayflies-speak.md b/.changeset/few-mayflies-speak.md new file mode 100644 index 0000000000..1db7728fde --- /dev/null +++ b/.changeset/few-mayflies-speak.md @@ -0,0 +1,21 @@ +--- +"effect": minor +--- + +add Semaphore.withPermitsIfAvailable + +You can now use `Semaphore.withPermitsIfAvailable` to run an Effect only if the +Semaphore has enough permits available. This is useful when you want to run an +Effect only if you can acquire a permit without blocking. + +It will return an `Option.Some` with the result of the Effect if the permits were +available, or `None` if they were not. + +```ts +import { Effect } from "effect" + +Effect.gen(function* () { + const semaphore = yield* Effect.makeSemaphore(1) + semaphore.withPermitsIfAvailable(1)(Effect.void) +}) +``` diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts index e667425550..f58f064728 100644 --- a/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -5374,6 +5374,8 @@ export interface Permit { export interface Semaphore { /** when the given amount of permits are available, run the effect and release the permits when finished */ withPermits(permits: number): (self: Effect) => Effect + /** only if the given permits are available, run the effect and release the permits when finished */ + withPermitsIfAvailable(permits: number): (self: Effect) => Effect, E, R> /** take the given amount of permits, suspending if they are not yet available */ take(permits: number): Effect /** release the given amount of permits, and return the resulting available permits */ diff --git a/packages/effect/src/internal/effect/circular.ts b/packages/effect/src/internal/effect/circular.ts index cff08b7670..a1c790964f 100644 --- a/packages/effect/src/internal/effect/circular.ts +++ b/packages/effect/src/internal/effect/circular.ts @@ -93,6 +93,17 @@ class Semaphore { (permits) => fiberRuntime.ensuring(restore(self), this.release(permits)) ) ) + + readonly withPermitsIfAvailable = (n: number) => (self: Effect.Effect) => + core.uninterruptibleMask((restore) => + core.suspend(() => { + if (this.free < n) { + return effect.succeedNone + } + this.taken += n + return fiberRuntime.ensuring(restore(effect.asSome(self)), this.release(n)) + }) + ) } /** @internal */ From 321d652af1f5a59564f0d9e036ceb49d82231c36 Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Fri, 13 Sep 2024 04:00:59 +0300 Subject: [PATCH 13/22] add Logger.withLeveledConsole (#3540) --- .changeset/calm-houses-brake.md | 20 ++++++++ packages/effect/src/Logger.ts | 22 +++++++++ packages/effect/src/internal/fiberRuntime.ts | 22 +++++++++ packages/effect/test/Logger.test.ts | 52 +++++++++++++++++++- 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 .changeset/calm-houses-brake.md diff --git a/.changeset/calm-houses-brake.md b/.changeset/calm-houses-brake.md new file mode 100644 index 0000000000..d98f0b2b8d --- /dev/null +++ b/.changeset/calm-houses-brake.md @@ -0,0 +1,20 @@ +--- +"effect": minor +--- + +add `Logger.withLeveledConsole` + +In browsers and different platforms, `console.error` renders differently than `console.info`. This helps to distinguish between different levels of logging. `Logger.withLeveledConsole` takes any logger and calls the respective `Console` method based on the log level. For instance, `Effect.logError` will call `Console.error` and `Effect.logInfo` will call `Console.info`. + +To use it, you can replace the default logger with a `Logger.withLeveledConsole` logger: + +```ts +import { Logger, Effect } from "effect" + +const loggerLayer = Logger.withLeveledConsole(Logger.stringLogger) + +Effect.gen(function* () { + yield* Effect.logError("an error") + yield* Effect.logInfo("an info") +}).pipe(Effect.provide(loggerLayer)) +``` diff --git a/packages/effect/src/Logger.ts b/packages/effect/src/Logger.ts index 29eb1300ac..60a0625185 100644 --- a/packages/effect/src/Logger.ts +++ b/packages/effect/src/Logger.ts @@ -238,6 +238,28 @@ export const batched: { */ export const withConsoleLog: (self: Logger) => Logger = fiberRuntime.loggerWithConsoleLog +/** + * Takes a `Logger` and returns a logger that calls the respective `Console` method + * based on the log level. + * + * @example + * import { Logger, Effect } from "effect" + * + * const loggerLayer = Logger.replace( + * Logger.defaultLogger, + * Logger.withLeveledConsole(Logger.stringLogger), + * ) + * + * Effect.gen(function* () { + * yield* Effect.logError("an error") + * yield* Effect.logInfo("an info") + * }).pipe(Effect.provide(loggerLayer)) + * + * @since 3.8.0 + * @category console + */ +export const withLeveledConsole: (self: Logger) => Logger = fiberRuntime.loggerWithLeveledLog + /** * @since 2.0.0 * @category console diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index f75f7d3e80..c11b56da68 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -1400,6 +1400,28 @@ export const loggerWithConsoleLog = (self: Logger): Logger Context.get(services, consoleTag).unsafe.log(self.log(opts)) }) +/** @internal */ +export const loggerWithLeveledLog = (self: Logger): Logger => + internalLogger.makeLogger((opts) => { + const services = FiberRefs.getOrDefault(opts.context, defaultServices.currentServices) + const unsafeLogger = Context.get(services, consoleTag).unsafe + switch (opts.logLevel._tag) { + case "Debug": + return unsafeLogger.debug(self.log(opts)) + case "Info": + return unsafeLogger.info(self.log(opts)) + case "Trace": + return unsafeLogger.trace(self.log(opts)) + case "Warning": + return unsafeLogger.warn(self.log(opts)) + case "Error": + case "Fatal": + return unsafeLogger.error(self.log(opts)) + default: + return unsafeLogger.log(self.log(opts)) + } + }) + /** @internal */ export const loggerWithConsoleError = (self: Logger): Logger => internalLogger.makeLogger((opts) => { diff --git a/packages/effect/test/Logger.test.ts b/packages/effect/test/Logger.test.ts index 969527b2cb..7ba1e474f7 100644 --- a/packages/effect/test/Logger.test.ts +++ b/packages/effect/test/Logger.test.ts @@ -8,8 +8,10 @@ import * as HashMap from "effect/HashMap" import { logLevelInfo } from "effect/internal/core" import * as List from "effect/List" import * as Logger from "effect/Logger" +import * as LogLevel from "effect/LogLevel" import * as LogSpan from "effect/LogSpan" -import { afterEach, assert, beforeEach, describe, expect, it, vi } from "vitest" +import { assert, describe, expect, it } from "effect/test/utils/extend" +import { afterEach, beforeEach, vi } from "vitest" describe("Logger", () => { it("isLogger", () => { @@ -21,6 +23,54 @@ describe("Logger", () => { }) }) +describe("withLeveledConsole", () => { + it.effect("calls the respsective Console functions on a given level", () => + Effect.gen(function*() { + const c = yield* Effect.console + const logs: Array<{ level: string; value: unknown }> = [] + const pusher = (level: string) => (value: unknown) => { + logs.push({ level, value }) + } + const newConsole: typeof c = { + ...c, + unsafe: { + ...c.unsafe, + log: pusher("log"), + warn: pusher("warn"), + error: pusher("error"), + info: pusher("info"), + debug: pusher("debug"), + trace: pusher("trace") + } + } + + const logger = Logger.make((o) => String(o.message)).pipe(Logger.withLeveledConsole) + yield* Effect.gen(function*() { + yield* Effect.log("log plain") + yield* Effect.logInfo("log info") + yield* Effect.logWarning("log warn") + yield* Effect.logError("log err") + yield* Effect.logFatal("log fatal") + yield* Effect.logDebug("log debug") + yield* Effect.logTrace("log trace") + }).pipe( + Effect.provide(Logger.replace(Logger.defaultLogger, logger)), + Logger.withMinimumLogLevel(LogLevel.Trace), + Effect.withConsole(newConsole) + ) + + expect(logs).toEqual([ + { level: "info", value: "log plain" }, + { level: "info", value: "log info" }, + { level: "warn", value: "log warn" }, + { level: "error", value: "log err" }, + { level: "error", value: "log fatal" }, + { level: "debug", value: "log debug" }, + { level: "trace", value: "log trace" } + ]) + })) +}) + describe("stringLogger", () => { beforeEach(() => { vi.useFakeTimers() From 7aa48ebd8b7b0bbd04af875731352b4cd43e726d Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 13 Sep 2024 13:17:08 +1200 Subject: [PATCH 14/22] add Effect.makeLatch (#3571) --- .changeset/lucky-eagles-speak.md | 24 ++++++++ packages/effect/src/Effect.ts | 50 +++++++++++++++ .../effect/src/internal/effect/circular.ts | 61 +++++++++++++++++++ packages/effect/test/Effect/latch.test.ts | 33 ++++++++++ 4 files changed, 168 insertions(+) create mode 100644 .changeset/lucky-eagles-speak.md create mode 100644 packages/effect/test/Effect/latch.test.ts diff --git a/.changeset/lucky-eagles-speak.md b/.changeset/lucky-eagles-speak.md new file mode 100644 index 0000000000..c25560826a --- /dev/null +++ b/.changeset/lucky-eagles-speak.md @@ -0,0 +1,24 @@ +--- +"effect": minor +--- + +add Effect.makeLatch, for creating a simple async latch + +```ts +import { Effect } from "effect" + +Effect.gen(function* () { + // Create a latch, starting in the closed state + const latch = yield* Effect.makeLatch(false) + + // Fork a fiber that logs "open sesame" when the latch is opened + const fiber = yield* Effect.log("open sesame").pipe( + latch.whenOpen, + Effect.fork + ) + + // Open the latch + yield* latch.open + yield* fiber.await +}) +``` diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts index f58f064728..090039deb2 100644 --- a/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -5400,6 +5400,56 @@ export const unsafeMakeSemaphore: (permits: number) => Semaphore = circular.unsa */ export const makeSemaphore: (permits: number) => Effect = circular.makeSemaphore +// ------------------------------------------------------------------------------------- +// latch +// ------------------------------------------------------------------------------------- + +/** + * @category latch + * @since 3.8.0 + */ +export interface Latch { + /** open the latch, releasing all fibers waiting on it */ + readonly open: Effect + /** release all fibers waiting on the latch, without opening it */ + readonly release: Effect + /** wait for the latch to be opened */ + readonly await: Effect + /** close the latch */ + readonly close: Effect + /** only run the given effect when the latch is open */ + readonly whenOpen: (self: Effect) => Effect +} + +/** + * @category latch + * @since 3.8.0 + */ +export const unsafeMakeLatch: (open?: boolean | undefined) => Latch = circular.unsafeMakeLatch + +/** + * @category latch + * @since 3.8.0 + * @example + * import { Effect } from "effect" + * + * Effect.gen(function*() { + * // Create a latch, starting in the closed state + * const latch = yield* Effect.makeLatch(false) + * + * // Fork a fiber that logs "open sesame" when the latch is opened + * const fiber = yield* Effect.log("open sesame").pipe( + * latch.whenOpen, + * Effect.fork + * ) + * + * // Open the latch + * yield* latch.open + * yield* fiber.await + * }) + */ +export const makeLatch: (open?: boolean | undefined) => Effect = circular.makeLatch + // ------------------------------------------------------------------------------------- // execution // ------------------------------------------------------------------------------------- diff --git a/packages/effect/src/internal/effect/circular.ts b/packages/effect/src/internal/effect/circular.ts index a1c790964f..53a75fea5c 100644 --- a/packages/effect/src/internal/effect/circular.ts +++ b/packages/effect/src/internal/effect/circular.ts @@ -112,6 +112,67 @@ export const unsafeMakeSemaphore = (permits: number): Semaphore => new Semaphore /** @internal */ export const makeSemaphore = (permits: number) => core.sync(() => unsafeMakeSemaphore(permits)) +class Latch implements Effect.Latch { + waiters: Array<(_: Effect.Effect) => void> = [] + scheduled = false + constructor(private isOpen: boolean) {} + + private unsafeSchedule(fiber: Fiber.RuntimeFiber) { + if (this.scheduled || this.waiters.length === 0) { + return core.void + } + this.scheduled = true + fiber.currentScheduler.scheduleTask(this.flushWaiters, fiber.getFiberRef(core.currentSchedulingPriority)) + return core.void + } + private flushWaiters = () => { + this.scheduled = false + const waiters = this.waiters + this.waiters = [] + for (let i = 0; i < waiters.length; i++) { + waiters[i](core.exitVoid) + } + } + + open = core.withFiberRuntime((fiber) => { + if (this.isOpen) { + return core.void + } + this.isOpen = true + return this.unsafeSchedule(fiber) + }) + release = core.withFiberRuntime((fiber) => { + if (this.isOpen) { + return core.void + } + return this.unsafeSchedule(fiber) + }) + await = core.unsafeAsync((resume) => { + if (this.isOpen) { + return resume(core.void) + } + this.waiters.push(resume) + return core.sync(() => { + const index = this.waiters.indexOf(resume) + if (index !== -1) { + this.waiters.splice(index, 1) + } + }) + }) + close = core.sync(() => { + this.isOpen = false + }) + whenOpen = (self: Effect.Effect): Effect.Effect => { + return core.zipRight(this.await, self) + } +} + +/** @internal */ +export const unsafeMakeLatch = (open?: boolean | undefined): Effect.Latch => new Latch(open ?? false) + +/** @internal */ +export const makeLatch = (open?: boolean | undefined) => core.sync(() => unsafeMakeLatch(open)) + /** @internal */ export const awaitAllChildren = (self: Effect.Effect): Effect.Effect => ensuringChildren(self, fiberRuntime.fiberAwaitAll) diff --git a/packages/effect/test/Effect/latch.test.ts b/packages/effect/test/Effect/latch.test.ts new file mode 100644 index 0000000000..e85d56184e --- /dev/null +++ b/packages/effect/test/Effect/latch.test.ts @@ -0,0 +1,33 @@ +import { Effect, Exit } from "effect" +import { assert, describe, it } from "effect/test/utils/extend" + +describe("Latch", () => { + it.effect("open works", () => + Effect.gen(function*() { + const latch = yield* Effect.makeLatch() + let fiber = yield* latch.await.pipe( + Effect.fork + ) + yield* Effect.yieldNow() + assert.isNull(fiber.unsafePoll()) + yield* latch.open + assert.deepStrictEqual(yield* fiber.await, Exit.void) + + fiber = yield* latch.await.pipe( + Effect.fork + ) + yield* Effect.yieldNow() + assert.deepStrictEqual(fiber.unsafePoll(), Exit.void) + + yield* latch.close + fiber = yield* Effect.void.pipe( + latch.whenOpen, + Effect.fork + ) + yield* Effect.yieldNow() + assert.isNull(fiber.unsafePoll()) + + yield* latch.release + assert.deepStrictEqual(yield* fiber.await, Exit.void) + })) +}) From 5f78f17e75ebd71e2c01383bb94c685d5a7c1a43 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 12 Sep 2024 17:32:20 +1200 Subject: [PATCH 15/22] add Cause.Annotated --- .changeset/strong-moose-tickle.md | 5 + packages/effect/src/Cause.ts | 92 ++++- packages/effect/src/Context.ts | 8 + packages/effect/src/internal/cause.ts | 323 ++++++++++++++---- packages/effect/src/internal/context.ts | 10 + packages/effect/src/internal/core.ts | 56 ++- packages/effect/src/internal/opCodes/cause.ts | 6 + 7 files changed, 386 insertions(+), 114 deletions(-) create mode 100644 .changeset/strong-moose-tickle.md diff --git a/.changeset/strong-moose-tickle.md b/.changeset/strong-moose-tickle.md new file mode 100644 index 0000000000..c5dde2ac2e --- /dev/null +++ b/.changeset/strong-moose-tickle.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +add Cause.Annotated diff --git a/packages/effect/src/Cause.ts b/packages/effect/src/Cause.ts index 7c7ce6c9a2..c587dbbf5c 100644 --- a/packages/effect/src/Cause.ts +++ b/packages/effect/src/Cause.ts @@ -23,6 +23,7 @@ */ import type * as Channel from "./Channel.js" import type * as Chunk from "./Chunk.js" +import type * as Context from "./Context.js" import type * as Effect from "./Effect.js" import type * as Either from "./Either.js" import type * as Equal from "./Equal.js" @@ -166,6 +167,7 @@ export type Cause = | Interrupt | Sequential | Parallel + | Annotated /** * @since 2.0.0 @@ -190,12 +192,13 @@ export declare namespace Cause { * @category models */ export interface CauseReducer { - emptyCase(context: C): Z - failCase(context: C, error: E): Z - dieCase(context: C, defect: unknown): Z - interruptCase(context: C, fiberId: FiberId.FiberId): Z - sequentialCase(context: C, left: Z, right: Z): Z - parallelCase(context: C, left: Z, right: Z): Z + emptyCase(context: C, annotations: Context.Context): Z + failCase(context: C, error: E, annotations: Context.Context): Z + dieCase(context: C, defect: unknown, annotations: Context.Context): Z + interruptCase(context: C, fiberId: FiberId.FiberId, annotations: Context.Context): Z + sequentialCase(context: C, left: Z, right: Z, annotations: Context.Context): Z + parallelCase(context: C, left: Z, right: Z, annotations: Context.Context): Z + annotatedCase(context: C, out: Z, annotations: Context.Context, parentAnnotations: Context.Context): Z } /** @@ -358,6 +361,7 @@ export interface Die extends Cause.Variance, Equal.Equal, Pipeable, Inspe export interface Interrupt extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { readonly _tag: "Interrupt" readonly fiberId: FiberId.FiberId + readonly originSpan: Span | undefined } /** @@ -397,6 +401,19 @@ export interface Sequential extends Cause.Variance, Equal.Equal, Pipea readonly right: Cause } +/** + * The `Annotated` cause represents a `Cause` which has been annotated with + * additional context. + * + * @since 3.8.0 + * @category models + */ +export interface Annotated extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { + readonly _tag: "Annotated" + readonly cause: Cause + readonly context: Context.Context +} + /** * Constructs a new `Empty` cause. * @@ -429,6 +446,28 @@ export const die: (defect: unknown) => Cause = internal.die */ export const interrupt: (fiberId: FiberId.FiberId) => Cause = internal.interrupt +/** + * Constructs a new `Annotated` cause from the specified `cause` and `context`. + * + * @since 3.8.0 + * @category constructors + */ +export const annotated: { + (context: Context.Context): (self: Cause) => Cause + (self: Cause, context: Context.Context): Cause +} = internal.annotated + +/** + * Constructs a `Annotated` cause from the specified `cause`, `tag` and `value`. + * + * @since 3.8.0 + * @category constructors + */ +export const annotate: { + (tag: Context.Tag, value: S): (self: Cause) => Cause + (self: Cause, tag: Context.Tag, value: S): Cause +} = internal.annotate + /** * Constructs a new `Parallel` cause from the specified `left` and `right` * causes. @@ -509,6 +548,15 @@ export const isSequentialType: (self: Cause) => self is Sequential = in */ export const isParallelType: (self: Cause) => self is Parallel = internal.isParallelType +/** + * Returns `true` if the specified `Cause` is a `Annotated` type, `false` + * otherwise. + * + * @since 3.8.0 + * @category refinements + */ +export const isAnnotatedType: (self: Cause) => self is Annotated = internal.isAnnotatedType + /** * Returns the size of the cause, calculated as the number of individual `Cause` * nodes found in the `Cause` semiring structure. @@ -586,6 +634,16 @@ export const defects: (self: Cause) => Chunk.Chunk = internal.def */ export const interruptors: (self: Cause) => HashSet.HashSet = internal.interruptors +/** + * Return the merged `Context` of all `Annotated` causes in the specified cause. + * + * Outer annotations shadow inner annotations. + * + * @since 3.8.0 + * @category getters + */ +export const annotations: (self: Cause) => Context.Context = internal.annotations + /** * Returns the `E` associated with the first `Fail` in this `Cause`, if one * exists. @@ -787,22 +845,24 @@ export const match: { ( options: { readonly onEmpty: Z - readonly onFail: (error: E) => Z - readonly onDie: (defect: unknown) => Z - readonly onInterrupt: (fiberId: FiberId.FiberId) => Z - readonly onSequential: (left: Z, right: Z) => Z - readonly onParallel: (left: Z, right: Z) => Z + readonly onFail: (error: E, annotations: Context.Context) => Z + readonly onDie: (defect: unknown, annotations: Context.Context) => Z + readonly onInterrupt: (fiberId: FiberId.FiberId, annotations: Context.Context) => Z + readonly onSequential: (left: Z, right: Z, annotations: Context.Context) => Z + readonly onParallel: (left: Z, right: Z, annotations: Context.Context) => Z + readonly onAnnotated: (out: Z, context: Context.Context, annotations: Context.Context) => Z } ): (self: Cause) => Z ( self: Cause, options: { readonly onEmpty: Z - readonly onFail: (error: E) => Z - readonly onDie: (defect: unknown) => Z - readonly onInterrupt: (fiberId: FiberId.FiberId) => Z - readonly onSequential: (left: Z, right: Z) => Z - readonly onParallel: (left: Z, right: Z) => Z + readonly onFail: (error: E, annotations: Context.Context) => Z + readonly onDie: (defect: unknown, annotations: Context.Context) => Z + readonly onInterrupt: (fiberId: FiberId.FiberId, annotations: Context.Context) => Z + readonly onSequential: (left: Z, right: Z, annotations: Context.Context) => Z + readonly onParallel: (left: Z, right: Z, annotations: Context.Context) => Z + readonly onAnnotated: (out: Z, context: Context.Context, annotations: Context.Context) => Z } ): Z } = internal.match diff --git a/packages/effect/src/Context.ts b/packages/effect/src/Context.ts index 9cf2440bdd..576ec9deea 100644 --- a/packages/effect/src/Context.ts +++ b/packages/effect/src/Context.ts @@ -393,6 +393,14 @@ export const omit: >>( ) => (self: Context) => Context }[keyof S]>> = internal.omit +/** + * @since 3.8.0 + */ +export const has: { + (tag: Tag): (self: Context) => self is Context + (self: Context, tag: Tag): self is Context +} = internal.has + /** * @since 2.0.0 * @category constructors diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index bb705011d4..d66a510520 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -1,6 +1,7 @@ import * as Arr from "../Array.js" import type * as Cause from "../Cause.js" import * as Chunk from "../Chunk.js" +import * as Context from "../Context.js" import * as Either from "../Either.js" import * as Equal from "../Equal.js" import type * as FiberId from "../FiberId.js" @@ -64,6 +65,8 @@ const proto = { case "Sequential": case "Parallel": return { _id: "Cause", _tag: this._tag, left: toJSON(this.left), right: toJSON(this.right) } + case "Annotated": + return toJSON(this.cause) } }, toString(this: Cause.Cause) { @@ -90,7 +93,7 @@ export const fail = (error: E): Cause.Cause => { const o = Object.create(proto) o._tag = OpCodes.OP_FAIL o.error = error - return o + return rehydrateAnnotations(o, error) } /** @internal */ @@ -98,7 +101,7 @@ export const die = (defect: unknown): Cause.Cause => { const o = Object.create(proto) o._tag = OpCodes.OP_DIE o.defect = defect - return o + return rehydrateAnnotations(o, defect) } /** @internal */ @@ -106,7 +109,7 @@ export const interrupt = (fiberId: FiberId.FiberId): Cause.Cause => { const o = Object.create(proto) o._tag = OpCodes.OP_INTERRUPT o.fiberId = fiberId - return o + return rehydrateAnnotations(o, fiberId) } /** @internal */ @@ -127,6 +130,48 @@ export const sequential = (left: Cause.Cause, right: Cause.Cause): return o } +/** @internal */ +export const annotated = dual< + (context: Context.Context) => (self: Cause.Cause) => Cause.Cause, + (self: Cause.Cause, context: Context.Context) => Cause.Cause +>(2, (self, context) => { + const o = Object.create(proto) + o._tag = OpCodes.OP_ANNOTATED + if (self._tag === OpCodes.OP_ANNOTATED) { + o.context = Context.merge(self.context, context) + o.cause = propagateAnnotations(self.cause, context) + } else { + o.context = context + o.cause = propagateAnnotations(self, context) + } + return o +}) + +const annotatedPlain = (self: Cause.Cause, context: Context.Context): Cause.Cause => { + const o = Object.create(proto) + o._tag = OpCodes.OP_ANNOTATED + o.cause = self + o.context = context + return o +} + +/** @internal */ +export const annotate = dual< + (tag: Context.Tag, value: S) => (self: Cause.Cause) => Cause.Cause, + (self: Cause.Cause, tag: Context.Tag, value: S) => Cause.Cause +>(3, (self, tag, value) => { + const o = Object.create(proto) + o._tag = OpCodes.OP_ANNOTATED + if (self._tag === OpCodes.OP_ANNOTATED) { + o.context = Context.add(self.context, tag, value) + o.cause = propagateAnnotations(self.cause, o.context) + } else { + o.context = Context.make(tag, value) + o.cause = propagateAnnotations(self, o.context) + } + return o +}) + // ----------------------------------------------------------------------------- // Refinements // ----------------------------------------------------------------------------- @@ -153,6 +198,10 @@ export const isSequentialType = (self: Cause.Cause): self is Cause.Sequent /** @internal */ export const isParallelType = (self: Cause.Cause): self is Cause.Parallel => self._tag === OpCodes.OP_PARALLEL +/** @internal */ +export const isAnnotatedType = (self: Cause.Cause): self is Cause.Annotated => + self._tag === OpCodes.OP_ANNOTATED + // ----------------------------------------------------------------------------- // Getters // ----------------------------------------------------------------------------- @@ -167,6 +216,7 @@ export const isEmpty = (self: Cause.Cause): boolean => { } return reduce(self, true, (acc, cause) => { switch (cause._tag) { + case OpCodes.OP_ANNOTATED: case OpCodes.OP_EMPTY: { return Option.some(acc) } @@ -228,6 +278,25 @@ export const interruptors = (self: Cause.Cause): HashSet.HashSet(self: Cause.Cause): Context.Context => + Context.unsafeMake( + reduce( + self, + new Map(), + (map, cause) => { + if (cause._tag !== OpCodes.OP_ANNOTATED) return Option.none() + const innerMap = cause.context.unsafeMap + for (const [key, value] of innerMap) { + if (!map.has(key)) { + map.set(key, value) + } + } + return Option.some(map) + } + ) + ) + /** @internal */ export const failureOption = (self: Cause.Cause): Option.Option => find(self, (cause) => @@ -260,7 +329,7 @@ export const dieOption = (self: Cause.Cause): Option.Option => export const flipCauseOption = (self: Cause.Cause>): Option.Option> => match(self, { onEmpty: Option.some(empty), - onFail: (failureOption) => pipe(failureOption, Option.map(fail)), + onFail: (failureOption) => Option.map(failureOption, fail), onDie: (defect) => Option.some(die(defect)), onInterrupt: (fiberId) => Option.some(interrupt(fiberId)), onSequential: (left, right) => { @@ -286,7 +355,8 @@ export const flipCauseOption = (self: Cause.Cause>): Option. return Option.some(left.value) } return Option.none() - } + }, + onAnnotated: (cause, context) => Option.map(cause, annotated(context)) }) /** @internal */ @@ -326,7 +396,8 @@ export const keepDefects = (self: Cause.Cause): Option.Option cause }) /** @internal */ @@ -359,7 +430,8 @@ export const keepDefectsAndElectFailures = (self: Cause.Cause): Option.Opt return Option.some(right.value) } return Option.none() - } + }, + onAnnotated: (cause) => cause }) /** @internal */ @@ -388,7 +460,8 @@ export const linearize = (self: Cause.Cause): HashSet.HashSet parallel(leftCause, rightCause)) ) ) - ) + ), + onAnnotated: (cause, context) => HashSet.map(cause, annotated(context)) }) /** @internal */ @@ -399,7 +472,8 @@ export const stripFailures = (self: Cause.Cause): Cause.Cause => onDie: (defect) => die(defect), onInterrupt: (fiberId) => interrupt(fiberId), onSequential: sequential, - onParallel: parallel + onParallel: parallel, + onAnnotated: (cause, context) => annotated(cause, context) }) /** @internal */ @@ -410,7 +484,8 @@ export const electFailures = (self: Cause.Cause): Cause.Cause => onDie: (defect) => die(defect), onInterrupt: (fiberId) => interrupt(fiberId), onSequential: (left, right) => sequential(left, right), - onParallel: (left, right) => parallel(left, right) + onParallel: (left, right) => parallel(left, right), + onAnnotated: (cause, context) => annotated(cause, context) }) /** @internal */ @@ -449,7 +524,8 @@ export const stripSomeDefects = dual< return Option.some(right.value) } return Option.none() - } + }, + onAnnotated: (cause, context) => Option.map(cause, annotated(context)) })) // ----------------------------------------------------------------------------- @@ -483,7 +559,8 @@ export const flatMap = dual< onDie: (defect) => die(defect), onInterrupt: (fiberId) => interrupt(fiberId), onSequential: (left, right) => sequential(left, right), - onParallel: (left, right) => parallel(left, right) + onParallel: (left, right) => parallel(left, right), + onAnnotated: (cause, context) => annotated(cause, context) })) /** @internal */ @@ -513,9 +590,7 @@ export const contains = dual< if (that._tag === OpCodes.OP_EMPTY || self === that) { return true } - return reduce(self, false, (accumulator, cause) => { - return Option.some(accumulator || causeEquals(cause, that)) - }) + return reduce(self, false, (accumulator, cause) => Option.some(accumulator || causeEquals(cause, that))) }) /** @internal */ @@ -631,6 +706,10 @@ export const find = dual< stack.push(item.left) break } + case OpCodes.OP_ANNOTATED: { + stack.push(item.cause) + break + } } break } @@ -740,6 +819,10 @@ const evaluateCause = ( cause = cause.left break } + case OpCodes.OP_ANNOTATED: { + cause = cause.cause + break + } } } throw new Error(getBugErrorMessage("Cause.evaluateCauseLoop")) @@ -756,7 +839,8 @@ const SizeCauseReducer: Cause.CauseReducer = { dieCase: () => 1, interruptCase: () => 1, sequentialCase: (_, left, right) => left + right, - parallelCase: (_, left, right) => left + right + parallelCase: (_, left, right) => left + right, + annotatedCase: (_, count) => count } /** @internal */ @@ -766,7 +850,8 @@ const IsInterruptedOnlyCauseReducer: Cause.CauseReducer left && right, - parallelCase: (_, left, right) => left && right + parallelCase: (_, left, right) => left && right, + annotatedCase: (_, bool) => bool } /** @internal */ @@ -800,24 +885,41 @@ const FilterCauseReducer = ( return right } return empty + }, + annotatedCase: (_, cause, context) => { + if (predicate(cause)) { + return annotated(cause, context) + } + return empty } }) /** @internal */ -type CauseCase = SequentialCase | ParallelCase +type CauseCase = SequentialCase | ParallelCase | AnnotatedCase const OP_SEQUENTIAL_CASE = "SequentialCase" const OP_PARALLEL_CASE = "ParallelCase" +const OP_ANNOTATED_CASE = "AnnotatedCase" + /** @internal */ interface SequentialCase { readonly _tag: typeof OP_SEQUENTIAL_CASE + readonly annotations: Context.Context } /** @internal */ interface ParallelCase { readonly _tag: typeof OP_PARALLEL_CASE + readonly annotations: Context.Context +} + +/** @internal */ +interface AnnotatedCase { + readonly _tag: typeof OP_ANNOTATED_CASE + readonly context: Context.Context + readonly annotations: Context.Context } /** @internal */ @@ -825,40 +927,46 @@ export const match = dual< ( options: { readonly onEmpty: Z - readonly onFail: (error: E) => Z - readonly onDie: (defect: unknown) => Z - readonly onInterrupt: (fiberId: FiberId.FiberId) => Z - readonly onSequential: (left: Z, right: Z) => Z - readonly onParallel: (left: Z, right: Z) => Z + readonly onFail: (error: E, annotations: Context.Context) => Z + readonly onDie: (defect: unknown, annotations: Context.Context) => Z + readonly onInterrupt: (fiberId: FiberId.FiberId, annotations: Context.Context) => Z + readonly onSequential: (left: Z, right: Z, annotations: Context.Context) => Z + readonly onParallel: (left: Z, right: Z, annotations: Context.Context) => Z + readonly onAnnotated: (out: Z, context: Context.Context, annotations: Context.Context) => Z } ) => (self: Cause.Cause) => Z, ( self: Cause.Cause, options: { readonly onEmpty: Z - readonly onFail: (error: E) => Z - readonly onDie: (defect: unknown) => Z - readonly onInterrupt: (fiberId: FiberId.FiberId) => Z - readonly onSequential: (left: Z, right: Z) => Z - readonly onParallel: (left: Z, right: Z) => Z + readonly onFail: (error: E, annotations: Context.Context) => Z + readonly onDie: (defect: unknown, annotations: Context.Context) => Z + readonly onInterrupt: (fiberId: FiberId.FiberId, annotations: Context.Context) => Z + readonly onSequential: (left: Z, right: Z, annotations: Context.Context) => Z + readonly onParallel: (left: Z, right: Z, annotations: Context.Context) => Z + readonly onAnnotated: (out: Z, context: Context.Context, annotations: Context.Context) => Z } ) => Z ->(2, (self, { onDie, onEmpty, onFail, onInterrupt, onParallel, onSequential }) => { - return reduceWithContext(self, void 0, { - emptyCase: () => onEmpty, - failCase: (_, error) => onFail(error), - dieCase: (_, defect) => onDie(defect), - interruptCase: (_, fiberId) => onInterrupt(fiberId), - sequentialCase: (_, left, right) => onSequential(left, right), - parallelCase: (_, left, right) => onParallel(left, right) - }) -}) +>( + 2, + (self, { onAnnotated, onDie, onEmpty, onFail, onInterrupt, onParallel, onSequential }) => + reduceWithContext(self, void 0, { + emptyCase: () => onEmpty, + failCase: (_, error, annotations) => onFail(error, annotations), + dieCase: (_, defect, annotations) => onDie(defect, annotations), + interruptCase: (_, fiberId, annotations) => onInterrupt(fiberId, annotations), + sequentialCase: (_, left, right, annotations) => onSequential(left, right, annotations), + parallelCase: (_, left, right, annotations) => onParallel(left, right, annotations), + annotatedCase: (_, out, context, annotations) => onAnnotated(out, context, annotations) + }) +) /** @internal */ export const reduce = dual< (zero: Z, pf: (accumulator: Z, cause: Cause.Cause) => Option.Option) => (self: Cause.Cause) => Z, (self: Cause.Cause, zero: Z, pf: (accumulator: Z, cause: Cause.Cause) => Option.Option) => Z >(3, (self: Cause.Cause, zero: Z, pf: (accumulator: Z, cause: Cause.Cause) => Option.Option) => { + let annotations = Context.empty() let accumulator: Z = zero let cause: Cause.Cause | undefined = self const causes: Array> = [] @@ -876,6 +984,11 @@ export const reduce = dual< cause = cause.left break } + case OpCodes.OP_ANNOTATED: { + annotations = Context.merge(annotations, cause.context) + cause = cause.cause + break + } default: { cause = undefined break @@ -893,37 +1006,44 @@ export const reduceWithContext = dual< (context: C, reducer: Cause.CauseReducer) => (self: Cause.Cause) => Z, (self: Cause.Cause, context: C, reducer: Cause.CauseReducer) => Z >(3, (self: Cause.Cause, context: C, reducer: Cause.CauseReducer) => { + let annotations = Context.empty() const input: Array> = [self] const output: Array> = [] while (input.length > 0) { const cause = input.pop()! switch (cause._tag) { case OpCodes.OP_EMPTY: { - output.push(Either.right(reducer.emptyCase(context))) + output.push(Either.right(reducer.emptyCase(context, annotations))) break } case OpCodes.OP_FAIL: { - output.push(Either.right(reducer.failCase(context, cause.error))) + output.push(Either.right(reducer.failCase(context, cause.error, annotations))) break } case OpCodes.OP_DIE: { - output.push(Either.right(reducer.dieCase(context, cause.defect))) + output.push(Either.right(reducer.dieCase(context, cause.defect, annotations))) break } case OpCodes.OP_INTERRUPT: { - output.push(Either.right(reducer.interruptCase(context, cause.fiberId))) + output.push(Either.right(reducer.interruptCase(context, cause.fiberId, annotations))) break } case OpCodes.OP_SEQUENTIAL: { input.push(cause.right) input.push(cause.left) - output.push(Either.left({ _tag: OP_SEQUENTIAL_CASE })) + output.push(Either.left({ _tag: OP_SEQUENTIAL_CASE, annotations })) break } case OpCodes.OP_PARALLEL: { input.push(cause.right) input.push(cause.left) - output.push(Either.left({ _tag: OP_PARALLEL_CASE })) + output.push(Either.left({ _tag: OP_PARALLEL_CASE, annotations })) + break + } + case OpCodes.OP_ANNOTATED: { + input.push(cause.cause) + output.push(Either.left({ _tag: OP_ANNOTATED_CASE, context: cause.context, annotations })) + annotations = Context.merge(annotations, cause.context) break } } @@ -937,14 +1057,20 @@ export const reduceWithContext = dual< case OP_SEQUENTIAL_CASE: { const left = accumulator.pop()! const right = accumulator.pop()! - const value = reducer.sequentialCase(context, left, right) + const value = reducer.sequentialCase(context, left, right, either.left.annotations) accumulator.push(value) break } case OP_PARALLEL_CASE: { const left = accumulator.pop()! const right = accumulator.pop()! - const value = reducer.parallelCase(context, left, right) + const value = reducer.parallelCase(context, left, right, either.left.annotations) + accumulator.push(value) + break + } + case OP_ANNOTATED_CASE: { + const out = accumulator.pop()! + const value = reducer.annotatedCase(context, out, either.left.context, either.left.annotations) accumulator.push(value) break } @@ -998,14 +1124,14 @@ const renderErrorCause = (cause: PrettyError, prefix: string) => { class PrettyError extends globalThis.Error implements Cause.PrettyError { span: undefined | Span = undefined - constructor(originalError: unknown) { + constructor(originalError: unknown, annotations: Context.Context) { const originalErrorIsObject = typeof originalError === "object" && originalError !== null const prevLimit = Error.stackTraceLimit Error.stackTraceLimit = 1 super( prettyErrorMessage(originalError), originalErrorIsObject && "cause" in originalError && typeof originalError.cause !== "undefined" - ? { cause: new PrettyError(originalError.cause) } + ? { cause: new PrettyError(originalError.cause, Context.empty()) } : undefined ) if (this.message === "") { @@ -1013,10 +1139,10 @@ class PrettyError extends globalThis.Error implements Cause.PrettyError { } Error.stackTraceLimit = prevLimit this.name = originalError instanceof Error ? originalError.name : "Error" + if (Context.has(annotations, FailureSpan)) { + this.span = Context.get(annotations, FailureSpan) + } if (originalErrorIsObject) { - if (spanSymbol in originalError) { - this.span = originalError[spanSymbol] as Span - } Object.keys(originalError).forEach((key) => { if (!(key in this)) { // @ts-expect-error @@ -1122,19 +1248,94 @@ const prettyErrorStack = (message: string, stack: string, span?: Span | undefine return out.join("\n") } -const spanSymbol = Symbol.for("effect/SpanAnnotation") - /** @internal */ export const prettyErrors = (cause: Cause.Cause): Array => - reduceWithContext(cause, void 0, { + reduceWithContext(cause, new Map(), { emptyCase: (): Array => [], - dieCase: (_, unknownError) => { - return [new PrettyError(unknownError)] - }, - failCase: (_, error) => { - return [new PrettyError(error)] - }, + dieCase: (_, unknownError, context) => [new PrettyError(unknownError, context)], + failCase: (_, error, context) => [new PrettyError(error, context)], interruptCase: () => [], parallelCase: (_, l, r) => [...l, ...r], - sequentialCase: (_, l, r) => [...l, ...r] + sequentialCase: (_, l, r) => [...l, ...r], + annotatedCase: (_, out) => out }) + +// ----------------------------------------------------------------------------- +// Annotations +// ----------------------------------------------------------------------------- + +/** @internal */ +export const OriginalInstance = Context.GenericTag<"OriginalInstance", unknown>("effect/Cause/OriginalInstance") + +/** @internal */ +export const FailureSpan = Context.GenericTag<"FailureSpan", Span>("effect/Cause/FailureSpan") + +/** @internal */ +export const InterruptorSpan = Context.GenericTag<"InterruptorSpan", Span>("effect/Cause/InterruptorSpan") + +const originalAnnotationsSymbol = Symbol.for("effect/Cause/originalAnnotationsSymbol") + +/* @internal */ +export const originalAnnotations = (obj: E): Context.Context | undefined => { + if (hasProperty(obj, originalAnnotationsSymbol)) { + // @ts-expect-error + return obj[originalAnnotationsSymbol] + } + return undefined +} + +function maybeAddAnnotations(obj: E, annotations: Context.Context): E { + if ( + typeof obj !== "object" || + obj === null || + annotations.unsafeMap.size === 0 + ) { + return obj + } + let context = Context.add(annotations, OriginalInstance, obj) + if (originalAnnotationsSymbol in obj) { + context = Context.merge(context, obj[originalAnnotationsSymbol] as Context.Context) + } + return new Proxy(obj, { + has(target, p) { + return p === originalAnnotationsSymbol || p in target + }, + get(target, p) { + if (p === originalAnnotationsSymbol) { + return context + } + // @ts-expect-error + return target[p] + } + }) +} + +const AnnotationsReducer: Cause.CauseReducer, unknown, Cause.Cause> = { + emptyCase: (_) => empty, + failCase: (context, error, annotations) => fail(maybeAddAnnotations(error, Context.merge(context, annotations))), + dieCase: (context, defect, annotations) => die(maybeAddAnnotations(defect, Context.merge(context, annotations))), + interruptCase: (context, fiberId, annotations) => + interrupt(maybeAddAnnotations(fiberId, Context.merge(context, annotations))), + sequentialCase: (_, left, right) => sequential(left, right), + parallelCase: (_, left, right) => parallel(left, right), + annotatedCase: (_, cause, annotations) => annotated(cause, annotations) +} + +const propagateAnnotations = (self: Cause.Cause, context: Context.Context): Cause.Cause => + reduceWithContext(self, context, AnnotationsReducer) + +const rehydrateAnnotations = (self: Cause.Cause, obj: unknown): Cause.Cause => { + if (hasProperty(obj, originalAnnotationsSymbol)) { + return annotatedPlain(self, (obj as any)[originalAnnotationsSymbol]) + } + return self +} + +/** @internal */ +export const originalAnnotation = (self: E, tag: Context.Tag, fallback: S): S => { + const context = originalAnnotations(self) + if (context === undefined || !context.unsafeMap.has(tag.key)) { + return fallback + } + return context.unsafeMap.get(tag.key) as S +} diff --git a/packages/effect/src/internal/context.ts b/packages/effect/src/internal/context.ts index 1fe348d8a5..59f242f133 100644 --- a/packages/effect/src/internal/context.ts +++ b/packages/effect/src/internal/context.ts @@ -272,3 +272,13 @@ export const omit = } return makeContext(newEnv) } + +/** @internal */ +export const has = dual< + (tag: C.Tag) => (self: C.Context) => self is C.Context, + (self: C.Context, tag: C.Tag) => self is C.Context +>( + 2, + (self: C.Context, tag: C.Tag): self is C.Context => + self.unsafeMap.has(tag.key) +) diff --git a/packages/effect/src/internal/core.ts b/packages/effect/src/internal/core.ts index 4b3350fba3..ebd299095b 100644 --- a/packages/effect/src/internal/core.ts +++ b/packages/effect/src/internal/core.ts @@ -662,45 +662,25 @@ export const checkInterruptible = ( f: (isInterruptible: boolean) => Effect.Effect ): Effect.Effect => withFiberRuntime((_, status) => f(_runtimeFlags.interruption(status.runtimeFlags))) -const spanSymbol = Symbol.for("effect/SpanAnnotation") -const originalSymbol = Symbol.for("effect/OriginalAnnotation") - /* @internal */ -export const originalInstance = (obj: E): E => { - if (hasProperty(obj, originalSymbol)) { - // @ts-expect-error - return obj[originalSymbol] - } - return obj -} +export const originalInstance = (obj: E): E => + internalCause.originalAnnotation(obj, internalCause.OriginalInstance, obj) as E /* @internal */ -const capture = (obj: E & object, span: Option.Option): E => { - if (Option.isSome(span)) { - return new Proxy(obj, { - has(target, p) { - return p === spanSymbol || p === originalSymbol || p in target - }, - get(target, p) { - if (p === spanSymbol) { - return span.value - } - if (p === originalSymbol) { - return obj - } - // @ts-expect-error - return target[p] - } - }) - } - return obj -} +const capture = (cause: Cause.Cause): Effect.Effect => + withFiberRuntime((fiber) => { + const span = currentSpanFromFiber(fiber) + if (span._tag === "Some") { + cause = internalCause.annotate(cause, internalCause.FailureSpan, span.value) + } + return failCause(cause) + }) /* @internal */ export const die = (defect: unknown): Effect.Effect => - isObject(defect) && !(spanSymbol in defect) ? - withFiberRuntime((fiber) => failCause(internalCause.die(capture(defect, currentSpanFromFiber(fiber))))) - : failCause(internalCause.die(defect)) + !internalCause.originalAnnotations(defect) ? + capture(internalCause.die(defect)) : + failCause(internalCause.die(defect)) /* @internal */ export const dieMessage = (message: string): Effect.Effect => @@ -725,9 +705,9 @@ export const exit = (self: Effect.Effect): Effect.Effect(error: E): Effect.Effect => - isObject(error) && !(spanSymbol in error) ? - withFiberRuntime((fiber) => failCause(internalCause.fail(capture(error, currentSpanFromFiber(fiber))))) - : failCause(internalCause.fail(error)) + !internalCause.originalAnnotations(error) ? + capture(internalCause.fail(error)) : + failCause(internalCause.fail(error)) /* @internal */ export const failSync = (evaluate: LazyArg): Effect.Effect => flatMap(sync(evaluate), fail) @@ -1005,7 +985,9 @@ export const interrupt: Effect.Effect = flatMap(fiberId, (fiberId) => int /* @internal */ export const interruptWith = (fiberId: FiberId.FiberId): Effect.Effect => - failCause(internalCause.interrupt(fiberId)) + !internalCause.originalAnnotations(fiberId) ? + capture(internalCause.interrupt(fiberId)) : + failCause(internalCause.interrupt(fiberId)) /* @internal */ export const interruptible = (self: Effect.Effect): Effect.Effect => { diff --git a/packages/effect/src/internal/opCodes/cause.ts b/packages/effect/src/internal/opCodes/cause.ts index b8c1d85fe3..19edfd140e 100644 --- a/packages/effect/src/internal/opCodes/cause.ts +++ b/packages/effect/src/internal/opCodes/cause.ts @@ -33,3 +33,9 @@ export const OP_SEQUENTIAL = "Sequential" as const /** @internal */ export type OP_SEQUENTIAL = typeof OP_SEQUENTIAL + +/** @internal */ +export const OP_ANNOTATED = "Annotated" as const + +/** @internal */ +export type OP_ANNOTATED = typeof OP_ANNOTATED From ea8d388bb66afde26ed272e346c10cfb6fc7f75e Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 12 Sep 2024 21:29:15 +1200 Subject: [PATCH 16/22] move span capture to failCause --- packages/effect/src/Cause.ts | 20 +- packages/effect/src/Context.ts | 5 + packages/effect/src/FiberHandle.ts | 3 +- packages/effect/src/FiberMap.ts | 3 +- packages/effect/src/FiberSet.ts | 3 +- packages/effect/src/internal/cause.ts | 245 ++++++++++-------- packages/effect/src/internal/context.ts | 3 + packages/effect/src/internal/core.ts | 44 ++-- packages/effect/src/internal/fiberRuntime.ts | 15 +- .../effect/test/Effect/interruption.test.ts | 19 ++ 10 files changed, 217 insertions(+), 143 deletions(-) diff --git a/packages/effect/src/Cause.ts b/packages/effect/src/Cause.ts index c587dbbf5c..f41e60e91e 100644 --- a/packages/effect/src/Cause.ts +++ b/packages/effect/src/Cause.ts @@ -1043,4 +1043,22 @@ export const prettyErrors: (cause: Cause) => Array = internal * @since 2.0.0 * @category errors */ -export const originalError: (obj: E) => E = core.originalInstance +export const originalError: (obj: E) => E = internal.originalInstance + +/** + * @since 3.8.0 + * @category annotations + */ +export const FailureSpan: Context.Tag<"FailureSpan", Span> = internal.FailureSpan + +/** + * @since 3.8.0 + * @category annotations + */ +export const InterruptorSpan: Context.Tag<"InterruptorSpan", Span> = internal.InterruptorSpan + +/** + * @since 3.8.0 + * @category annotations + */ +export const withAnnotationPropagationDisabled: (f: () => A) => A = internal.withAnnotationPropagationDisabled diff --git a/packages/effect/src/Context.ts b/packages/effect/src/Context.ts index 576ec9deea..5258af70d4 100644 --- a/packages/effect/src/Context.ts +++ b/packages/effect/src/Context.ts @@ -401,6 +401,11 @@ export const has: { (self: Context, tag: Tag): self is Context } = internal.has +/** + * @since 3.8.0 + */ +export const isEmpty: (self: Context) => boolean = internal.isEmpty + /** * @since 2.0.0 * @category constructors diff --git a/packages/effect/src/FiberHandle.ts b/packages/effect/src/FiberHandle.ts index 4392172a82..6944ae520b 100644 --- a/packages/effect/src/FiberHandle.ts +++ b/packages/effect/src/FiberHandle.ts @@ -151,7 +151,8 @@ const isInternalInterruption = Cause.reduceWithContext(undefined, { dieCase: constFalse, interruptCase: (_, fiberId) => HashSet.has(FiberId.ids(fiberId), internalFiberIdId), sequentialCase: (_, left, right) => left || right, - parallelCase: (_, left, right) => left || right + parallelCase: (_, left, right) => left || right, + annotatedCase: (_, out) => out }) /** diff --git a/packages/effect/src/FiberMap.ts b/packages/effect/src/FiberMap.ts index 57c0fff373..4d473d43ca 100644 --- a/packages/effect/src/FiberMap.ts +++ b/packages/effect/src/FiberMap.ts @@ -166,7 +166,8 @@ const isInternalInterruption = Cause.reduceWithContext(undefined, { dieCase: constFalse, interruptCase: (_, fiberId) => HashSet.has(FiberId.ids(fiberId), internalFiberIdId), sequentialCase: (_, left, right) => left || right, - parallelCase: (_, left, right) => left || right + parallelCase: (_, left, right) => left || right, + annotatedCase: (_, out) => out }) /** diff --git a/packages/effect/src/FiberSet.ts b/packages/effect/src/FiberSet.ts index 53e0a50a8c..afb8562f71 100644 --- a/packages/effect/src/FiberSet.ts +++ b/packages/effect/src/FiberSet.ts @@ -155,7 +155,8 @@ const isInternalInterruption = Cause.reduceWithContext(undefined, { dieCase: constFalse, interruptCase: (_, fiberId) => HashSet.has(FiberId.ids(fiberId), internalFiberIdId), sequentialCase: (_, left, right) => left || right, - parallelCase: (_, left, right) => left || right + parallelCase: (_, left, right) => left || right, + annotatedCase: (_, out) => out }) /** diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index d66a510520..1205e2a024 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -66,7 +66,7 @@ const proto = { case "Parallel": return { _id: "Cause", _tag: this._tag, left: toJSON(this.left), right: toJSON(this.right) } case "Annotated": - return toJSON(this.cause) + return { _id: "Cause", _tag: this._tag, cause: this.cause.toJSON(), context: this.context.toJSON() } } }, toString(this: Cause.Cause) { @@ -138,8 +138,8 @@ export const annotated = dual< const o = Object.create(proto) o._tag = OpCodes.OP_ANNOTATED if (self._tag === OpCodes.OP_ANNOTATED) { - o.context = Context.merge(self.context, context) - o.cause = propagateAnnotations(self.cause, context) + o.context = Context.merge(context, self.context) + o.cause = propagateAnnotations(self.cause, o.context) } else { o.context = context o.cause = propagateAnnotations(self, context) @@ -147,30 +147,11 @@ export const annotated = dual< return o }) -const annotatedPlain = (self: Cause.Cause, context: Context.Context): Cause.Cause => { - const o = Object.create(proto) - o._tag = OpCodes.OP_ANNOTATED - o.cause = self - o.context = context - return o -} - /** @internal */ export const annotate = dual< (tag: Context.Tag, value: S) => (self: Cause.Cause) => Cause.Cause, (self: Cause.Cause, tag: Context.Tag, value: S) => Cause.Cause ->(3, (self, tag, value) => { - const o = Object.create(proto) - o._tag = OpCodes.OP_ANNOTATED - if (self._tag === OpCodes.OP_ANNOTATED) { - o.context = Context.add(self.context, tag, value) - o.cause = propagateAnnotations(self.cause, o.context) - } else { - o.context = Context.make(tag, value) - o.cause = propagateAnnotations(self, o.context) - } - return o -}) +>(3, (self, tag, value) => annotated(self, Context.make(tag, value))) // ----------------------------------------------------------------------------- // Refinements @@ -1005,91 +986,95 @@ export const reduce = dual< export const reduceWithContext = dual< (context: C, reducer: Cause.CauseReducer) => (self: Cause.Cause) => Z, (self: Cause.Cause, context: C, reducer: Cause.CauseReducer) => Z ->(3, (self: Cause.Cause, context: C, reducer: Cause.CauseReducer) => { - let annotations = Context.empty() - const input: Array> = [self] - const output: Array> = [] - while (input.length > 0) { - const cause = input.pop()! - switch (cause._tag) { - case OpCodes.OP_EMPTY: { - output.push(Either.right(reducer.emptyCase(context, annotations))) - break - } - case OpCodes.OP_FAIL: { - output.push(Either.right(reducer.failCase(context, cause.error, annotations))) - break - } - case OpCodes.OP_DIE: { - output.push(Either.right(reducer.dieCase(context, cause.defect, annotations))) - break - } - case OpCodes.OP_INTERRUPT: { - output.push(Either.right(reducer.interruptCase(context, cause.fiberId, annotations))) - break - } - case OpCodes.OP_SEQUENTIAL: { - input.push(cause.right) - input.push(cause.left) - output.push(Either.left({ _tag: OP_SEQUENTIAL_CASE, annotations })) - break - } - case OpCodes.OP_PARALLEL: { - input.push(cause.right) - input.push(cause.left) - output.push(Either.left({ _tag: OP_PARALLEL_CASE, annotations })) - break - } - case OpCodes.OP_ANNOTATED: { - input.push(cause.cause) - output.push(Either.left({ _tag: OP_ANNOTATED_CASE, context: cause.context, annotations })) - annotations = Context.merge(annotations, cause.context) - break - } - } - } - const accumulator: Array = [] - while (output.length > 0) { - const either = output.pop()! - switch (either._tag) { - case "Left": { - switch (either.left._tag) { - case OP_SEQUENTIAL_CASE: { - const left = accumulator.pop()! - const right = accumulator.pop()! - const value = reducer.sequentialCase(context, left, right, either.left.annotations) - accumulator.push(value) +>( + 3, + (self: Cause.Cause, context: C, reducer: Cause.CauseReducer) => + withAnnotationPropagationDisabled(() => { + let annotations = Context.empty() + const input: Array> = [self] + const output: Array> = [] + while (input.length > 0) { + const cause = input.pop()! + switch (cause._tag) { + case OpCodes.OP_EMPTY: { + output.push(Either.right(reducer.emptyCase(context, annotations))) break } - case OP_PARALLEL_CASE: { - const left = accumulator.pop()! - const right = accumulator.pop()! - const value = reducer.parallelCase(context, left, right, either.left.annotations) - accumulator.push(value) + case OpCodes.OP_FAIL: { + output.push(Either.right(reducer.failCase(context, cause.error, annotations))) break } - case OP_ANNOTATED_CASE: { - const out = accumulator.pop()! - const value = reducer.annotatedCase(context, out, either.left.context, either.left.annotations) - accumulator.push(value) + case OpCodes.OP_DIE: { + output.push(Either.right(reducer.dieCase(context, cause.defect, annotations))) + break + } + case OpCodes.OP_INTERRUPT: { + output.push(Either.right(reducer.interruptCase(context, cause.fiberId, annotations))) + break + } + case OpCodes.OP_SEQUENTIAL: { + input.push(cause.right) + input.push(cause.left) + output.push(Either.left({ _tag: OP_SEQUENTIAL_CASE, annotations })) + break + } + case OpCodes.OP_PARALLEL: { + input.push(cause.right) + input.push(cause.left) + output.push(Either.left({ _tag: OP_PARALLEL_CASE, annotations })) + break + } + case OpCodes.OP_ANNOTATED: { + input.push(cause.cause) + output.push(Either.left({ _tag: OP_ANNOTATED_CASE, context: cause.context, annotations })) + annotations = Context.merge(annotations, cause.context) break } } - break } - case "Right": { - accumulator.push(either.right) - break + const accumulator: Array = [] + while (output.length > 0) { + const either = output.pop()! + switch (either._tag) { + case "Left": { + switch (either.left._tag) { + case OP_SEQUENTIAL_CASE: { + const left = accumulator.pop()! + const right = accumulator.pop()! + const value = reducer.sequentialCase(context, left, right, either.left.annotations) + accumulator.push(value) + break + } + case OP_PARALLEL_CASE: { + const left = accumulator.pop()! + const right = accumulator.pop()! + const value = reducer.parallelCase(context, left, right, either.left.annotations) + accumulator.push(value) + break + } + case OP_ANNOTATED_CASE: { + const out = accumulator.pop()! + const value = reducer.annotatedCase(context, out, either.left.context, either.left.annotations) + accumulator.push(value) + break + } + } + break + } + case "Right": { + accumulator.push(either.right) + break + } + } } - } - } - if (accumulator.length === 0) { - throw new Error( - "BUG: Cause.reduceWithContext - please report an issue at https://github.com/Effect-TS/effect/issues" - ) - } - return accumulator.pop()! -}) + if (accumulator.length === 0) { + throw new Error( + "BUG: Cause.reduceWithContext - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } + return accumulator.pop()! + }) +) // ----------------------------------------------------------------------------- // Pretty Printing @@ -1264,9 +1249,6 @@ export const prettyErrors = (cause: Cause.Cause): Array => // Annotations // ----------------------------------------------------------------------------- -/** @internal */ -export const OriginalInstance = Context.GenericTag<"OriginalInstance", unknown>("effect/Cause/OriginalInstance") - /** @internal */ export const FailureSpan = Context.GenericTag<"FailureSpan", Span>("effect/Cause/FailureSpan") @@ -1274,6 +1256,10 @@ export const FailureSpan = Context.GenericTag<"FailureSpan", Span>("effect/Cause export const InterruptorSpan = Context.GenericTag<"InterruptorSpan", Span>("effect/Cause/InterruptorSpan") const originalAnnotationsSymbol = Symbol.for("effect/Cause/originalAnnotationsSymbol") +const originalInstanceSymbol = Symbol.for("effect/Cause/originalInstanceSymbol") +const annotationState = globalValue("effect/Cause/annotationState", () => ({ + disablePropagation: false +})) /* @internal */ export const originalAnnotations = (obj: E): Context.Context | undefined => { @@ -1284,7 +1270,7 @@ export const originalAnnotations = (obj: E): Context.Context | undefin return undefined } -function maybeAddAnnotations(obj: E, annotations: Context.Context): E { +function addOriginalAnnotations(obj: E, annotations: Context.Context): E { if ( typeof obj !== "object" || obj === null || @@ -1292,17 +1278,21 @@ function maybeAddAnnotations(obj: E, annotations: Context.Context): E ) { return obj } - let context = Context.add(annotations, OriginalInstance, obj) if (originalAnnotationsSymbol in obj) { - context = Context.merge(context, obj[originalAnnotationsSymbol] as Context.Context) + annotations = Context.merge(annotations, obj[originalAnnotationsSymbol] as Context.Context) + } + if (originalInstanceSymbol in obj) { + obj = obj[originalInstanceSymbol] as E } - return new Proxy(obj, { + return new Proxy(obj as E & object, { has(target, p) { - return p === originalAnnotationsSymbol || p in target + return p === originalAnnotationsSymbol || p === originalInstanceSymbol || p in target }, get(target, p) { - if (p === originalAnnotationsSymbol) { - return context + if (p === originalInstanceSymbol) { + return obj + } else if (p === originalAnnotationsSymbol) { + return annotations } // @ts-expect-error return target[p] @@ -1312,25 +1302,38 @@ function maybeAddAnnotations(obj: E, annotations: Context.Context): E const AnnotationsReducer: Cause.CauseReducer, unknown, Cause.Cause> = { emptyCase: (_) => empty, - failCase: (context, error, annotations) => fail(maybeAddAnnotations(error, Context.merge(context, annotations))), - dieCase: (context, defect, annotations) => die(maybeAddAnnotations(defect, Context.merge(context, annotations))), + failCase: (context, error, annotations) => fail(addOriginalAnnotations(error, Context.merge(context, annotations))), + dieCase: (context, defect, annotations) => die(addOriginalAnnotations(defect, Context.merge(context, annotations))), interruptCase: (context, fiberId, annotations) => - interrupt(maybeAddAnnotations(fiberId, Context.merge(context, annotations))), + interrupt(addOriginalAnnotations(fiberId, Context.merge(context, annotations))), sequentialCase: (_, left, right) => sequential(left, right), parallelCase: (_, left, right) => parallel(left, right), - annotatedCase: (_, cause, annotations) => annotated(cause, annotations) + annotatedCase: (context, cause, annotations) => annotated(cause, Context.merge(context, annotations)) } const propagateAnnotations = (self: Cause.Cause, context: Context.Context): Cause.Cause => - reduceWithContext(self, context, AnnotationsReducer) + annotationState.disablePropagation ? self : reduceWithContext(self, context, AnnotationsReducer) const rehydrateAnnotations = (self: Cause.Cause, obj: unknown): Cause.Cause => { + if (annotationState.disablePropagation) { + return self + } if (hasProperty(obj, originalAnnotationsSymbol)) { - return annotatedPlain(self, (obj as any)[originalAnnotationsSymbol]) + return annotated(self, (obj as any)[originalAnnotationsSymbol]) } return self } +/** @internal */ +export const withAnnotationPropagationDisabled = (f: () => A): A => { + try { + annotationState.disablePropagation = true + return f() + } finally { + annotationState.disablePropagation = false + } +} + /** @internal */ export const originalAnnotation = (self: E, tag: Context.Tag, fallback: S): S => { const context = originalAnnotations(self) @@ -1339,3 +1342,15 @@ export const originalAnnotation = (self: E, tag: Context.Tag, fal } return context.unsafeMap.get(tag.key) as S } + +/** @internal */ +export const addOriginalAnnotation = (self: E, tag: Context.Tag, value: S): E => + addOriginalAnnotations(self, Context.make(tag, value)) + +/** @internal */ +export const originalInstance = (self: E): E => { + if (hasProperty(self, originalInstanceSymbol)) { + return self[originalInstanceSymbol] as E + } + return self +} diff --git a/packages/effect/src/internal/context.ts b/packages/effect/src/internal/context.ts index 59f242f133..d976613aff 100644 --- a/packages/effect/src/internal/context.ts +++ b/packages/effect/src/internal/context.ts @@ -282,3 +282,6 @@ export const has = dual< (self: C.Context, tag: C.Tag): self is C.Context => self.unsafeMap.has(tag.key) ) + +/** @internal */ +export const isEmpty = (self: C.Context): boolean => self.unsafeMap.size === 0 diff --git a/packages/effect/src/internal/core.ts b/packages/effect/src/internal/core.ts index ebd299095b..37494521ec 100644 --- a/packages/effect/src/internal/core.ts +++ b/packages/effect/src/internal/core.ts @@ -662,25 +662,21 @@ export const checkInterruptible = ( f: (isInterruptible: boolean) => Effect.Effect ): Effect.Effect => withFiberRuntime((_, status) => f(_runtimeFlags.interruption(status.runtimeFlags))) -/* @internal */ -export const originalInstance = (obj: E): E => - internalCause.originalAnnotation(obj, internalCause.OriginalInstance, obj) as E - -/* @internal */ const capture = (cause: Cause.Cause): Effect.Effect => withFiberRuntime((fiber) => { const span = currentSpanFromFiber(fiber) + let context = Context.empty() if (span._tag === "Some") { - cause = internalCause.annotate(cause, internalCause.FailureSpan, span.value) + context = Context.add(context, internalCause.FailureSpan, span.value) } - return failCause(cause) + cause = Context.isEmpty(context) ? cause : internalCause.annotated(cause, context) + const effect = new EffectPrimitiveFailure(OpCodes.OP_FAILURE) as any + effect.effect_instruction_i0 = cause + return effect }) /* @internal */ -export const die = (defect: unknown): Effect.Effect => - !internalCause.originalAnnotations(defect) ? - capture(internalCause.die(defect)) : - failCause(internalCause.die(defect)) +export const die = (defect: unknown): Effect.Effect => failCause(internalCause.die(defect)) /* @internal */ export const dieMessage = (message: string): Effect.Effect => @@ -704,19 +700,25 @@ export const exit = (self: Effect.Effect): Effect.Effect(error: E): Effect.Effect => - !internalCause.originalAnnotations(error) ? - capture(internalCause.fail(error)) : - failCause(internalCause.fail(error)) +export const fail = (error: E): Effect.Effect => failCause(internalCause.fail(error)) /* @internal */ export const failSync = (evaluate: LazyArg): Effect.Effect => flatMap(sync(evaluate), fail) /* @internal */ export const failCause = (cause: Cause.Cause): Effect.Effect => { - const effect = new EffectPrimitiveFailure(OpCodes.OP_FAILURE) as any - effect.effect_instruction_i0 = cause - return effect + switch (cause._tag) { + case "Fail": + case "Die": + case "Interrupt": { + return capture(cause) + } + default: { + const effect = new EffectPrimitiveFailure(OpCodes.OP_FAILURE) as any + effect.effect_instruction_i0 = cause + return effect + } + } } /* @internal */ @@ -985,9 +987,7 @@ export const interrupt: Effect.Effect = flatMap(fiberId, (fiberId) => int /* @internal */ export const interruptWith = (fiberId: FiberId.FiberId): Effect.Effect => - !internalCause.originalAnnotations(fiberId) ? - capture(internalCause.interrupt(fiberId)) : - failCause(internalCause.interrupt(fiberId)) + failCause(internalCause.interrupt(fiberId)) /* @internal */ export const interruptible = (self: Effect.Effect): Effect.Effect => { @@ -1534,7 +1534,7 @@ export const never: Effect.Effect = async(() => { /* @internal */ export const interruptFiber = (self: Fiber.Fiber): Effect.Effect> => - flatMap(fiberId, (fiberId) => pipe(self, interruptAsFiber(fiberId))) + flatMap(fiberId, (fiberId) => interruptAsFiber(self, fiberId)) /* @internal */ export const interruptAsFiber = dual< diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index c11b56da68..bb890e6372 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -492,14 +492,25 @@ export class FiberRuntime extends Effectable.Class { - return core.sync(() => this.tell(FiberMessage.interruptSignal(internalCause.interrupt(fiberId)))) + return core.withFiberRuntime((fiber) => { + const span = fiber.currentSpan + if (span && span._tag === "Span") { + fiberId = internalCause.addOriginalAnnotation(fiberId, internalCause.InterruptorSpan, span) + } + this.unsafeInterruptAsFork(fiberId) + return core.void + }) } /** * In the background, interrupts the fiber as if interrupted from the specified fiber. */ unsafeInterruptAsFork(fiberId: FiberId.FiberId) { - this.tell(FiberMessage.interruptSignal(internalCause.interrupt(fiberId))) + this.tell(FiberMessage.interruptSignal( + this.currentSpan && this.currentSpan._tag === "Span" + ? internalCause.annotate(internalCause.interrupt(fiberId), internalCause.FailureSpan, this.currentSpan) + : internalCause.interrupt(fiberId) + )) } /** diff --git a/packages/effect/test/Effect/interruption.test.ts b/packages/effect/test/Effect/interruption.test.ts index 2e8c8897fe..e7e84a49d6 100644 --- a/packages/effect/test/Effect/interruption.test.ts +++ b/packages/effect/test/Effect/interruption.test.ts @@ -1,3 +1,4 @@ +import { Context } from "effect" import * as Array from "effect/Array" import * as Cause from "effect/Cause" import * as Chunk from "effect/Chunk" @@ -604,4 +605,22 @@ describe("Effect", () => { yield* $(Fiber.interrupt(fiber)) assert.strictEqual(signal!.aborted, true) })) + + it.effect("span is captured", () => + Effect.gen(function*() { + const fiber = yield* Effect.never.pipe( + Effect.fork, + Effect.withSpan("span") + ) + yield* Fiber.interrupt(fiber).pipe( + Effect.withSpan("interruptor") + ) + const cause = yield* Fiber.join(fiber).pipe( + Effect.sandbox, + Effect.flip + ) + const annotations = Cause.annotations(cause) + assert.strictEqual(Context.unsafeGet(annotations, Cause.FailureSpan).name, "span") + assert.strictEqual(Context.unsafeGet(annotations, Cause.InterruptorSpan).name, "interruptor") + })) }) From 6150e9fe95e7f77da02da10720bba2a38c303805 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 12 Sep 2024 21:42:07 +1200 Subject: [PATCH 17/22] don't add proxy to FiberId --- packages/effect/src/internal/cause.ts | 5 ++--- packages/effect/src/internal/fiberRuntime.ts | 15 +++++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index 1205e2a024..d8ac761aa0 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -109,7 +109,7 @@ export const interrupt = (fiberId: FiberId.FiberId): Cause.Cause => { const o = Object.create(proto) o._tag = OpCodes.OP_INTERRUPT o.fiberId = fiberId - return rehydrateAnnotations(o, fiberId) + return o } /** @internal */ @@ -1304,8 +1304,7 @@ const AnnotationsReducer: Cause.CauseReducer, unknown, Ca emptyCase: (_) => empty, failCase: (context, error, annotations) => fail(addOriginalAnnotations(error, Context.merge(context, annotations))), dieCase: (context, defect, annotations) => die(addOriginalAnnotations(defect, Context.merge(context, annotations))), - interruptCase: (context, fiberId, annotations) => - interrupt(addOriginalAnnotations(fiberId, Context.merge(context, annotations))), + interruptCase: (_, fiberId) => interrupt(fiberId), sequentialCase: (_, left, right) => sequential(left, right), parallelCase: (_, left, right) => parallel(left, right), annotatedCase: (context, cause, annotations) => annotated(cause, Context.merge(context, annotations)) diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index bb890e6372..c152f63e22 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -493,11 +493,18 @@ export class FiberRuntime extends Effectable.Class { return core.withFiberRuntime((fiber) => { - const span = fiber.currentSpan - if (span && span._tag === "Span") { - fiberId = internalCause.addOriginalAnnotation(fiberId, internalCause.InterruptorSpan, span) + let context = Context.empty() + if (fiber.currentSpan && fiber.currentSpan._tag === "Span") { + context = Context.add(context, internalCause.InterruptorSpan, fiber.currentSpan) } - this.unsafeInterruptAsFork(fiberId) + if (this.currentSpan && this.currentSpan._tag === "Span") { + context = Context.add(context, internalCause.FailureSpan, this.currentSpan) + } + this.tell(FiberMessage.interruptSignal( + Context.isEmpty(context) + ? internalCause.interrupt(fiberId) + : internalCause.annotated(internalCause.interrupt(fiberId), context) + )) return core.void }) } From 5fa4648f3f9e87245967bb2caec496a5f345e0ca Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 12 Sep 2024 22:15:55 +1200 Subject: [PATCH 18/22] update Schema --- packages/effect/src/Cause.ts | 1 - packages/rpc/test/Router.test.ts | 15 +++---- packages/schema/src/Schema.ts | 40 +++++++++++++++---- .../schema/test/Schema/Cause/Cause.test.ts | 2 +- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/packages/effect/src/Cause.ts b/packages/effect/src/Cause.ts index f41e60e91e..8632dd4fe8 100644 --- a/packages/effect/src/Cause.ts +++ b/packages/effect/src/Cause.ts @@ -361,7 +361,6 @@ export interface Die extends Cause.Variance, Equal.Equal, Pipeable, Inspe export interface Interrupt extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { readonly _tag: "Interrupt" readonly fiberId: FiberId.FiberId - readonly originSpan: Span | undefined } /** diff --git a/packages/rpc/test/Router.test.ts b/packages/rpc/test/Router.test.ts index cb75b0c956..886506e10b 100644 --- a/packages/rpc/test/Router.test.ts +++ b/packages/rpc/test/Router.test.ts @@ -2,13 +2,14 @@ import { RpcResolver, RpcResolverNoStream, RpcRouter } from "@effect/rpc" import * as Rpc from "@effect/rpc/Rpc" import { Schema } from "@effect/schema" import * as S from "@effect/schema/Schema" +import { it } from "@effect/vitest" import * as Array from "effect/Array" import * as Chunk from "effect/Chunk" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import { flow, pipe } from "effect/Function" import * as Stream from "effect/Stream" -import { assert, describe, expect, it, test } from "vitest" +import { assert, describe, expect, test } from "vitest" interface Name { readonly _: unique symbol @@ -198,10 +199,10 @@ describe("Router", () => { value: "Hello, John!" }, { _tag: "Failure", - cause: { _tag: "Fail", error: { _tag: "SomeError", message: "fail" } } + cause: { _tag: "Annotated", cause: { _tag: "Fail", error: { _tag: "SomeError", message: "fail" } } } }, { _tag: "Failure", - cause: { _tag: "Fail", error: { _tag: "SomeError", message: "fail" } } + cause: { _tag: "Annotated", cause: { _tag: "Fail", error: { _tag: "SomeError", message: "fail" } } } }, { _tag: "Success", value: date.toISOString() @@ -247,10 +248,10 @@ describe("Router", () => { value: "Hello, John!" }, { _tag: "Failure", - cause: { _tag: "Fail", error: { _tag: "SomeError", message: "fail" } } + cause: { _tag: "Annotated", cause: { _tag: "Fail", error: { _tag: "SomeError", message: "fail" } } } }, { _tag: "Failure", - cause: { _tag: "Fail", error: { _tag: "SomeError", message: "fail" } } + cause: { _tag: "Annotated", cause: { _tag: "Fail", error: { _tag: "SomeError", message: "fail" } } } }, { _tag: "Success", value: date.toISOString() @@ -380,7 +381,7 @@ describe.each([{ ]) }).pipe(Effect.runPromise)) - test("stream fail", () => + it.effect("stream fail", () => Effect.gen(function*(_) { let n = 0 const result = yield* _( @@ -395,5 +396,5 @@ describe.each([{ ) assert.strictEqual(n, 2) assert.deepStrictEqual(result, new SomeError({ message: "fail" })) - }).pipe(Effect.runPromise)) + })) }) diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 5c8e92d3ad..c7c6592b2a 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -11,6 +11,7 @@ import * as cause_ from "effect/Cause" import * as chunk_ from "effect/Chunk" import * as config_ from "effect/Config" import * as configError_ from "effect/ConfigError" +import * as context_ from "effect/Context" import * as data_ from "effect/Data" import * as dateTime from "effect/DateTime" import * as duration_ from "effect/Duration" @@ -8310,6 +8311,10 @@ export type CauseEncoded = readonly left: CauseEncoded readonly right: CauseEncoded } + | { + readonly _tag: "Annotated" + readonly cause: CauseEncoded + } const causeDieEncoded = (defect: Schema) => Struct({ @@ -8346,6 +8351,12 @@ const causeSequentialEncoded = (causeEncoded: Schema(causeEncoded: Schema, CauseEncoded, R>) => + Struct({ + _tag: Literal("Annotated"), + cause: causeEncoded + }) + const causeEncoded = ( error: Schema, defect: Schema @@ -8357,15 +8368,16 @@ const causeEncoded = ( causeDieEncoded(defect), CauseInterruptEncoded, causeSequentialEncoded(recur), - causeParallelEncoded(recur) + causeParallelEncoded(recur), + causeAnnotatedEncoded(recur) ).annotations({ title: `CauseEncoded<${format(error)}>` }) return out } -const causeArbitrary = ( +const causeEncodedArbitrary = ( error: LazyArbitrary, defect: LazyArbitrary -): LazyArbitrary> => +): LazyArbitrary> => (fc) => fc.letrec((tie) => ({ Empty: fc.record({ _tag: fc.constant("Empty" as const) }), @@ -8374,15 +8386,23 @@ const causeArbitrary = ( Interrupt: fc.record({ _tag: fc.constant("Interrupt" as const), fiberId: fiberIdArbitrary(fc) }), Sequential: fc.record({ _tag: fc.constant("Sequential" as const), left: tie("Cause"), right: tie("Cause") }), Parallel: fc.record({ _tag: fc.constant("Parallel" as const), left: tie("Cause"), right: tie("Cause") }), + Annotated: fc.record({ _tag: fc.constant("Annotated" as const), cause: tie("Cause") }), Cause: fc.oneof( tie("Empty"), tie("Fail"), tie("Die"), tie("Interrupt"), tie("Sequential"), - tie("Parallel") - ) as any as fastCheck_.Arbitrary> - })).Cause.map(causeDecode) + tie("Parallel"), + tie("Annotated") + ) as any as fastCheck_.Arbitrary> + })).Cause + +const causeArbitrary = ( + error: LazyArbitrary, + defect: LazyArbitrary +): LazyArbitrary> => +(fc) => causeEncodedArbitrary(error, defect)(fc).map(causeDecode) const causePretty = (error: pretty_.Pretty): pretty_.Pretty> => (cause) => { const f = (cause: cause_.Cause): string => { @@ -8399,6 +8419,8 @@ const causePretty = (error: pretty_.Pretty): pretty_.Pretty(cause: CauseEncoded): cause_.Cause { return cause_.sequential(causeDecode(cause.left), causeDecode(cause.right)) case "Parallel": return cause_.parallel(causeDecode(cause.left), causeDecode(cause.right)) + case "Annotated": + return cause_.annotated(causeDecode(cause.cause), context_.empty()) } } @@ -8486,6 +8510,8 @@ function causeEncode(cause: cause_.Cause): CauseEncoded { left: causeEncode(cause.left), right: causeEncode(cause.right) } + case "Annotated": + return causeEncode(cause.cause) } } @@ -8626,7 +8652,7 @@ const exitArbitrary = ( ): LazyArbitrary> => (fc) => fc.oneof( - fc.record({ _tag: fc.constant("Failure" as const), cause: causeArbitrary(error, defect)(fc) }), + fc.record({ _tag: fc.constant("Failure" as const), cause: causeEncodedArbitrary(error, defect)(fc) }), fc.record({ _tag: fc.constant("Success" as const), value: value(fc) }) ).map(exitDecode) diff --git a/packages/schema/test/Schema/Cause/Cause.test.ts b/packages/schema/test/Schema/Cause/Cause.test.ts index 639828e56a..ae513efeaa 100644 --- a/packages/schema/test/Schema/Cause/Cause.test.ts +++ b/packages/schema/test/Schema/Cause/Cause.test.ts @@ -78,7 +78,7 @@ describe("Cause", () => { `(CauseEncoded <-> Cause) └─ Encoded side transformation failure └─ CauseEncoded - └─ { readonly _tag: "Empty" | "Fail" | "Die" | "Interrupt" | "Sequential" | "Parallel" } + └─ { readonly _tag: "Empty" | "Fail" | "Die" | "Interrupt" | "Sequential" | "Parallel" | "Annotated" } └─ ["_tag"] └─ is missing` ) From bbce77abe2e5c2cf87c69341a63e0607acb5abfd Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 13 Sep 2024 09:44:03 +1200 Subject: [PATCH 19/22] add InterruptedException --- packages/effect/src/internal/cause.ts | 44 ++++++++++++--- packages/effect/src/internal/fiberRuntime.ts | 8 ++- packages/effect/test/Cause.test.ts | 56 +++++++++++++++----- 3 files changed, 86 insertions(+), 22 deletions(-) diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index d8ac761aa0..04080bfce2 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -17,6 +17,7 @@ import { hasProperty, isFunction } from "../Predicate.js" import type { AnySpan, Span } from "../Tracer.js" import type { NoInfer } from "../Types.js" import { getBugErrorMessage } from "./errors.js" +import * as internalFiberId from "./fiberId.js" import * as OpCodes from "./opCodes/cause.js" // ----------------------------------------------------------------------------- @@ -1083,17 +1084,13 @@ export const reduceWithContext = dual< /** @internal */ export const pretty = (cause: Cause.Cause, options?: { readonly renderErrorCause?: boolean | undefined -}): string => { - if (isInterruptedOnly(cause)) { - return "All fibers interrupted without errors." - } - return prettyErrors(cause).map(function(e) { +}): string => + prettyErrors(cause).map(function(e) { if (options?.renderErrorCause !== true || e.cause === undefined) { return e.stack } return `${e.stack} {\n${renderErrorCause(e.cause as PrettyError, " ")}\n}` }).join("\n") -} const renderErrorCause = (cause: PrettyError, prefix: string) => { const lines = cause.stack!.split("\n") @@ -1145,6 +1142,37 @@ class PrettyError extends globalThis.Error implements Cause.PrettyError { } } +const makeErrorNoStack = (name: string, message: string, options?: ErrorOptions) => { + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 0 + const err = new Error(message, options) + err.name = name + Error.stackTraceLimit = limit + delete err.stack + return err +} + +const makeInterruptedException = (fiberId: FiberId.FiberId, context: Context.Context) => { + const threadName = internalFiberId.threadName(fiberId) + return new PrettyError( + makeErrorNoStack( + "InterruptedException", + "All fibers interrupted without errors.", + threadName.length === 0 + ? undefined + : { cause: makeInterruptedCause(threadName, Context.getOption(context, InterruptorSpan)) } + ), + context + ) +} + +const makeInterruptedCause = (threadName: string, span: Option.Option) => { + return new PrettyError( + makeErrorNoStack("InterruptedCause", `The fiber was interrupted by: ${threadName}.`), + Option.isSome(span) ? Context.make(FailureSpan, span.value) : Context.empty() + ) +} + /** * A utility function for generating human-readable error messages from a generic error of type `unknown`. * @@ -1235,11 +1263,11 @@ const prettyErrorStack = (message: string, stack: string, span?: Span | undefine /** @internal */ export const prettyErrors = (cause: Cause.Cause): Array => - reduceWithContext(cause, new Map(), { + reduceWithContext(cause, void 0, { emptyCase: (): Array => [], dieCase: (_, unknownError, context) => [new PrettyError(unknownError, context)], failCase: (_, error, context) => [new PrettyError(error, context)], - interruptCase: () => [], + interruptCase: (_, fiberId, context) => [makeInterruptedException(fiberId, context)], parallelCase: (_, l, r) => [...l, ...r], sequentialCase: (_, l, r) => [...l, ...r], annotatedCase: (_, out) => out diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index c152f63e22..ef647b5074 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -749,7 +749,7 @@ export class FiberRuntime extends Effectable.Class extends Effectable.Class { it("[internal] prettyErrorMessage", () => { @@ -48,7 +49,7 @@ describe("Cause", () => { describe("pretty", () => { it("Empty", () => { - expect(Cause.pretty(Cause.empty)).toEqual("All fibers interrupted without errors.") + expect(Cause.pretty(Cause.empty)).toEqual("") }) it("Fail", () => { @@ -84,14 +85,41 @@ describe("Cause", () => { }) it("Interrupt", () => { - expect(Cause.pretty(Cause.interrupt(FiberId.none))).toEqual("All fibers interrupted without errors.") - expect(Cause.pretty(Cause.interrupt(FiberId.runtime(1, 0)))).toEqual( - "All fibers interrupted without errors." + expect(Cause.pretty(Cause.interrupt(FiberId.none), { renderErrorCause: true })).toEqual( + `InterruptedException: All fibers interrupted without errors.` ) - expect(Cause.pretty(Cause.interrupt(FiberId.composite(FiberId.none, FiberId.runtime(1, 0))))).toEqual( - "All fibers interrupted without errors." + expect(Cause.pretty(Cause.interrupt(FiberId.runtime(1, 0)), { renderErrorCause: true })).toEqual( + `InterruptedException: All fibers interrupted without errors. { + [cause]: InterruptedCause: The fiber was interrupted by: #1. +}` + ) + expect( + Cause.pretty(Cause.interrupt(FiberId.composite(FiberId.none, FiberId.runtime(1, 0))), { + renderErrorCause: true + }) + ).toEqual( + `InterruptedException: All fibers interrupted without errors. { + [cause]: InterruptedCause: The fiber was interrupted by: #1. +}` ) }) + + it.effect("Interrupt with span", () => + Effect.gen(function*() { + const fiber = yield* Effect.never.pipe(Effect.fork, Effect.withSpan("span")) + yield* Effect.yieldNow() + yield* Fiber.interrupt(fiber).pipe( + Effect.withSpan("interruptor") + ) + const cause = yield* Fiber.join(fiber).pipe( + Effect.sandbox, + Effect.flip + ) + const rendered = Cause.pretty(cause, { renderErrorCause: true }) + expect(rendered.startsWith("InterruptedException: All fibers interrupted without errors.")).toBeTruthy() + expect(rendered).toMatch("at span (") + expect(rendered).toMatch("at interruptor (") + })) }) describe("toJSON", () => { @@ -202,7 +230,7 @@ describe("Cause", () => { describe("toString", () => { it("Empty", () => { - expect(String(Cause.empty)).toEqual(`All fibers interrupted without errors.`) + expect(String(Cause.empty)).toEqual("") }) it("Fail", () => { @@ -214,10 +242,14 @@ describe("Cause", () => { }) it("Interrupt", () => { - expect(String(Cause.interrupt(FiberId.none))).toEqual(`All fibers interrupted without errors.`) - expect(String(Cause.interrupt(FiberId.runtime(1, 0)))).toEqual(`All fibers interrupted without errors.`) + expect(String(Cause.interrupt(FiberId.none))).toEqual( + `InterruptedException: All fibers interrupted without errors.` + ) + expect(String(Cause.interrupt(FiberId.runtime(1, 0)))).toEqual( + `InterruptedException: All fibers interrupted without errors.` + ) expect(String(Cause.interrupt(FiberId.composite(FiberId.none, FiberId.runtime(1, 0))))).toEqual( - `All fibers interrupted without errors.` + `InterruptedException: All fibers interrupted without errors.` ) }) @@ -350,7 +382,7 @@ describe("Cause", () => { if (typeof window === "undefined") { // eslint-disable-next-line @typescript-eslint/no-var-requires const { inspect } = require("node:util") - expect(inspect(ex)).include("Cause.test.ts:348") + expect(inspect(ex)).include("Cause.test.ts:380") } }) }) From df9e7446eaf84f10bdc63c23dfedc065eb79a425 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 13 Sep 2024 10:11:25 +1200 Subject: [PATCH 20/22] move rehydration to capture --- packages/effect/src/Cause.ts | 6 - packages/effect/src/internal/cause.ts | 204 +++++++++++--------------- packages/effect/src/internal/core.ts | 22 ++- 3 files changed, 103 insertions(+), 129 deletions(-) diff --git a/packages/effect/src/Cause.ts b/packages/effect/src/Cause.ts index 8632dd4fe8..1873303505 100644 --- a/packages/effect/src/Cause.ts +++ b/packages/effect/src/Cause.ts @@ -1055,9 +1055,3 @@ export const FailureSpan: Context.Tag<"FailureSpan", Span> = internal.FailureSpa * @category annotations */ export const InterruptorSpan: Context.Tag<"InterruptorSpan", Span> = internal.InterruptorSpan - -/** - * @since 3.8.0 - * @category annotations - */ -export const withAnnotationPropagationDisabled: (f: () => A) => A = internal.withAnnotationPropagationDisabled diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index 04080bfce2..c7bdb24718 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -94,7 +94,7 @@ export const fail = (error: E): Cause.Cause => { const o = Object.create(proto) o._tag = OpCodes.OP_FAIL o.error = error - return rehydrateAnnotations(o, error) + return o } /** @internal */ @@ -102,7 +102,7 @@ export const die = (defect: unknown): Cause.Cause => { const o = Object.create(proto) o._tag = OpCodes.OP_DIE o.defect = defect - return rehydrateAnnotations(o, defect) + return o } /** @internal */ @@ -987,95 +987,91 @@ export const reduce = dual< export const reduceWithContext = dual< (context: C, reducer: Cause.CauseReducer) => (self: Cause.Cause) => Z, (self: Cause.Cause, context: C, reducer: Cause.CauseReducer) => Z ->( - 3, - (self: Cause.Cause, context: C, reducer: Cause.CauseReducer) => - withAnnotationPropagationDisabled(() => { - let annotations = Context.empty() - const input: Array> = [self] - const output: Array> = [] - while (input.length > 0) { - const cause = input.pop()! - switch (cause._tag) { - case OpCodes.OP_EMPTY: { - output.push(Either.right(reducer.emptyCase(context, annotations))) - break - } - case OpCodes.OP_FAIL: { - output.push(Either.right(reducer.failCase(context, cause.error, annotations))) - break - } - case OpCodes.OP_DIE: { - output.push(Either.right(reducer.dieCase(context, cause.defect, annotations))) - break - } - case OpCodes.OP_INTERRUPT: { - output.push(Either.right(reducer.interruptCase(context, cause.fiberId, annotations))) - break - } - case OpCodes.OP_SEQUENTIAL: { - input.push(cause.right) - input.push(cause.left) - output.push(Either.left({ _tag: OP_SEQUENTIAL_CASE, annotations })) - break - } - case OpCodes.OP_PARALLEL: { - input.push(cause.right) - input.push(cause.left) - output.push(Either.left({ _tag: OP_PARALLEL_CASE, annotations })) - break - } - case OpCodes.OP_ANNOTATED: { - input.push(cause.cause) - output.push(Either.left({ _tag: OP_ANNOTATED_CASE, context: cause.context, annotations })) - annotations = Context.merge(annotations, cause.context) +>(3, (self: Cause.Cause, context: C, reducer: Cause.CauseReducer) => { + let annotations = Context.empty() + const input: Array> = [self] + const output: Array> = [] + while (input.length > 0) { + const cause = input.pop()! + switch (cause._tag) { + case OpCodes.OP_EMPTY: { + output.push(Either.right(reducer.emptyCase(context, annotations))) + break + } + case OpCodes.OP_FAIL: { + output.push(Either.right(reducer.failCase(context, cause.error, annotations))) + break + } + case OpCodes.OP_DIE: { + output.push(Either.right(reducer.dieCase(context, cause.defect, annotations))) + break + } + case OpCodes.OP_INTERRUPT: { + output.push(Either.right(reducer.interruptCase(context, cause.fiberId, annotations))) + break + } + case OpCodes.OP_SEQUENTIAL: { + input.push(cause.right) + input.push(cause.left) + output.push(Either.left({ _tag: OP_SEQUENTIAL_CASE, annotations })) + break + } + case OpCodes.OP_PARALLEL: { + input.push(cause.right) + input.push(cause.left) + output.push(Either.left({ _tag: OP_PARALLEL_CASE, annotations })) + break + } + case OpCodes.OP_ANNOTATED: { + input.push(cause.cause) + output.push(Either.left({ _tag: OP_ANNOTATED_CASE, context: cause.context, annotations })) + annotations = Context.merge(annotations, cause.context) + break + } + } + } + const accumulator: Array = [] + while (output.length > 0) { + const either = output.pop()! + switch (either._tag) { + case "Left": { + switch (either.left._tag) { + case OP_SEQUENTIAL_CASE: { + const left = accumulator.pop()! + const right = accumulator.pop()! + const value = reducer.sequentialCase(context, left, right, either.left.annotations) + accumulator.push(value) break } - } - } - const accumulator: Array = [] - while (output.length > 0) { - const either = output.pop()! - switch (either._tag) { - case "Left": { - switch (either.left._tag) { - case OP_SEQUENTIAL_CASE: { - const left = accumulator.pop()! - const right = accumulator.pop()! - const value = reducer.sequentialCase(context, left, right, either.left.annotations) - accumulator.push(value) - break - } - case OP_PARALLEL_CASE: { - const left = accumulator.pop()! - const right = accumulator.pop()! - const value = reducer.parallelCase(context, left, right, either.left.annotations) - accumulator.push(value) - break - } - case OP_ANNOTATED_CASE: { - const out = accumulator.pop()! - const value = reducer.annotatedCase(context, out, either.left.context, either.left.annotations) - accumulator.push(value) - break - } - } + case OP_PARALLEL_CASE: { + const left = accumulator.pop()! + const right = accumulator.pop()! + const value = reducer.parallelCase(context, left, right, either.left.annotations) + accumulator.push(value) break } - case "Right": { - accumulator.push(either.right) + case OP_ANNOTATED_CASE: { + const out = accumulator.pop()! + const value = reducer.annotatedCase(context, out, either.left.context, either.left.annotations) + accumulator.push(value) break } } + break } - if (accumulator.length === 0) { - throw new Error( - "BUG: Cause.reduceWithContext - please report an issue at https://github.com/Effect-TS/effect/issues" - ) + case "Right": { + accumulator.push(either.right) + break } - return accumulator.pop()! - }) -) + } + } + if (accumulator.length === 0) { + throw new Error( + "BUG: Cause.reduceWithContext - please report an issue at https://github.com/Effect-TS/effect/issues" + ) + } + return accumulator.pop()! +}) // ----------------------------------------------------------------------------- // Pretty Printing @@ -1285,9 +1281,6 @@ export const InterruptorSpan = Context.GenericTag<"InterruptorSpan", Span>("effe const originalAnnotationsSymbol = Symbol.for("effect/Cause/originalAnnotationsSymbol") const originalInstanceSymbol = Symbol.for("effect/Cause/originalInstanceSymbol") -const annotationState = globalValue("effect/Cause/annotationState", () => ({ - disablePropagation: false -})) /* @internal */ export const originalAnnotations = (obj: E): Context.Context | undefined => { @@ -1328,36 +1321,17 @@ function addOriginalAnnotations(obj: E, annotations: Context.Context): }) } -const AnnotationsReducer: Cause.CauseReducer, unknown, Cause.Cause> = { - emptyCase: (_) => empty, - failCase: (context, error, annotations) => fail(addOriginalAnnotations(error, Context.merge(context, annotations))), - dieCase: (context, defect, annotations) => die(addOriginalAnnotations(defect, Context.merge(context, annotations))), - interruptCase: (_, fiberId) => interrupt(fiberId), - sequentialCase: (_, left, right) => sequential(left, right), - parallelCase: (_, left, right) => parallel(left, right), - annotatedCase: (context, cause, annotations) => annotated(cause, Context.merge(context, annotations)) -} - -const propagateAnnotations = (self: Cause.Cause, context: Context.Context): Cause.Cause => - annotationState.disablePropagation ? self : reduceWithContext(self, context, AnnotationsReducer) - -const rehydrateAnnotations = (self: Cause.Cause, obj: unknown): Cause.Cause => { - if (annotationState.disablePropagation) { - return self - } - if (hasProperty(obj, originalAnnotationsSymbol)) { - return annotated(self, (obj as any)[originalAnnotationsSymbol]) - } - return self -} - -/** @internal */ -export const withAnnotationPropagationDisabled = (f: () => A): A => { - try { - annotationState.disablePropagation = true - return f() - } finally { - annotationState.disablePropagation = false +const propagateAnnotations = (self: Cause.Cause, context: Context.Context): Cause.Cause => { + switch (self._tag) { + case "Die": { + return die(addOriginalAnnotations(self.defect, context)) + } + case "Fail": { + return fail(addOriginalAnnotations(self.error, context)) + } + default: { + return self + } } } diff --git a/packages/effect/src/internal/core.ts b/packages/effect/src/internal/core.ts index 37494521ec..b874298966 100644 --- a/packages/effect/src/internal/core.ts +++ b/packages/effect/src/internal/core.ts @@ -662,14 +662,19 @@ export const checkInterruptible = ( f: (isInterruptible: boolean) => Effect.Effect ): Effect.Effect => withFiberRuntime((_, status) => f(_runtimeFlags.interruption(status.runtimeFlags))) -const capture = (cause: Cause.Cause): Effect.Effect => +const capture = (cause: Cause.Cause, obj?: unknown): Effect.Effect => withFiberRuntime((fiber) => { - const span = currentSpanFromFiber(fiber) - let context = Context.empty() - if (span._tag === "Some") { - context = Context.add(context, internalCause.FailureSpan, span.value) + const originalAnnotations = internalCause.originalAnnotations(obj) + if (originalAnnotations) { + cause = internalCause.annotated(cause, originalAnnotations) + } else { + const span = currentSpanFromFiber(fiber) + let context = Context.empty() + if (span._tag === "Some") { + context = Context.add(context, internalCause.FailureSpan, span.value) + } + cause = Context.isEmpty(context) ? cause : internalCause.annotated(cause, context) } - cause = Context.isEmpty(context) ? cause : internalCause.annotated(cause, context) const effect = new EffectPrimitiveFailure(OpCodes.OP_FAILURE) as any effect.effect_instruction_i0 = cause return effect @@ -709,10 +714,11 @@ export const failSync = (evaluate: LazyArg): Effect.Effect => fl export const failCause = (cause: Cause.Cause): Effect.Effect => { switch (cause._tag) { case "Fail": + return capture(cause, cause.error) case "Die": - case "Interrupt": { + return capture(cause, cause.defect) + case "Interrupt": return capture(cause) - } default: { const effect = new EffectPrimitiveFailure(OpCodes.OP_FAILURE) as any effect.effect_instruction_i0 = cause From 759111c0a38305531ffe968137130464530598c2 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 13 Sep 2024 10:18:04 +1200 Subject: [PATCH 21/22] remove unused --- packages/effect/src/internal/cause.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index c7bdb24718..2054b163d9 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -1335,19 +1335,6 @@ const propagateAnnotations = (self: Cause.Cause, context: Context.Context< } } -/** @internal */ -export const originalAnnotation = (self: E, tag: Context.Tag, fallback: S): S => { - const context = originalAnnotations(self) - if (context === undefined || !context.unsafeMap.has(tag.key)) { - return fallback - } - return context.unsafeMap.get(tag.key) as S -} - -/** @internal */ -export const addOriginalAnnotation = (self: E, tag: Context.Tag, value: S): E => - addOriginalAnnotations(self, Context.make(tag, value)) - /** @internal */ export const originalInstance = (self: E): E => { if (hasProperty(self, originalInstanceSymbol)) { From 9b4e7603c3310c0af8563947f78239fc3ad1d46f Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 13 Sep 2024 10:43:15 +1200 Subject: [PATCH 22/22] make Exit.failCause refails work --- packages/effect/src/internal/cause.ts | 27 +++++++++++++++++---------- packages/effect/src/internal/core.ts | 20 ++++++-------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index 2054b163d9..317fb97c17 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -94,7 +94,7 @@ export const fail = (error: E): Cause.Cause => { const o = Object.create(proto) o._tag = OpCodes.OP_FAIL o.error = error - return o + return rehydrateAnnotations(o, error) } /** @internal */ @@ -102,7 +102,7 @@ export const die = (defect: unknown): Cause.Cause => { const o = Object.create(proto) o._tag = OpCodes.OP_DIE o.defect = defect - return o + return rehydrateAnnotations(o, defect) } /** @internal */ @@ -136,15 +136,19 @@ export const annotated = dual< (context: Context.Context) => (self: Cause.Cause) => Cause.Cause, (self: Cause.Cause, context: Context.Context) => Cause.Cause >(2, (self, context) => { + if (self._tag === OpCodes.OP_ANNOTATED && self.context === context) { + return self + } const o = Object.create(proto) o._tag = OpCodes.OP_ANNOTATED if (self._tag === OpCodes.OP_ANNOTATED) { o.context = Context.merge(context, self.context) - o.cause = propagateAnnotations(self.cause, o.context) + o.cause = self.cause } else { o.context = context - o.cause = propagateAnnotations(self, context) + o.cause = self } + propagateAnnotations(o.cause, o.context) return o }) @@ -1321,16 +1325,19 @@ function addOriginalAnnotations(obj: E, annotations: Context.Context): }) } -const propagateAnnotations = (self: Cause.Cause, context: Context.Context): Cause.Cause => { +const rehydrateAnnotations = (self: Cause.Cause, obj: unknown): Cause.Cause => { + const annotations = originalAnnotations(obj) + return annotations ? annotated(self, annotations) : self +} + +const propagateAnnotations = (self: Cause.Cause, context: Context.Context): void => { switch (self._tag) { case "Die": { - return die(addOriginalAnnotations(self.defect, context)) + ;(self as any).defect = addOriginalAnnotations(self.defect, context) + break } case "Fail": { - return fail(addOriginalAnnotations(self.error, context)) - } - default: { - return self + ;(self as any).error = addOriginalAnnotations(self.error, context) } } } diff --git a/packages/effect/src/internal/core.ts b/packages/effect/src/internal/core.ts index b874298966..a4c07b2ea0 100644 --- a/packages/effect/src/internal/core.ts +++ b/packages/effect/src/internal/core.ts @@ -662,21 +662,15 @@ export const checkInterruptible = ( f: (isInterruptible: boolean) => Effect.Effect ): Effect.Effect => withFiberRuntime((_, status) => f(_runtimeFlags.interruption(status.runtimeFlags))) -const capture = (cause: Cause.Cause, obj?: unknown): Effect.Effect => +const capture = (cause: Cause.Cause): Effect.Effect => withFiberRuntime((fiber) => { - const originalAnnotations = internalCause.originalAnnotations(obj) - if (originalAnnotations) { - cause = internalCause.annotated(cause, originalAnnotations) - } else { - const span = currentSpanFromFiber(fiber) - let context = Context.empty() - if (span._tag === "Some") { - context = Context.add(context, internalCause.FailureSpan, span.value) - } - cause = Context.isEmpty(context) ? cause : internalCause.annotated(cause, context) + const span = currentSpanFromFiber(fiber) + let context = Context.empty() + if (span._tag === "Some") { + context = Context.add(context, internalCause.FailureSpan, span.value) } const effect = new EffectPrimitiveFailure(OpCodes.OP_FAILURE) as any - effect.effect_instruction_i0 = cause + effect.effect_instruction_i0 = Context.isEmpty(context) ? cause : internalCause.annotated(cause, context) return effect }) @@ -714,9 +708,7 @@ export const failSync = (evaluate: LazyArg): Effect.Effect => fl export const failCause = (cause: Cause.Cause): Effect.Effect => { switch (cause._tag) { case "Fail": - return capture(cause, cause.error) case "Die": - return capture(cause, cause.defect) case "Interrupt": return capture(cause) default: {