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

Filter by extension

Filter by extension

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

Add type-level utils to asserting layer types
17 changes: 17 additions & 0 deletions .changeset/dynamic-idle-ttl-rcmap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"effect": minor
---

RcMap: support dynamic `idleTimeToLive` values per key

The `idleTimeToLive` option can now be a function that receives the key and returns a duration, allowing different TTL values for different resources.

```ts
const map = yield* RcMap.make({
lookup: (key: string) => acquireResource(key),
idleTimeToLive: (key: string) => {
if (key.startsWith("premium:")) return Duration.minutes(10)
return Duration.minutes(1)
}
})
```
17 changes: 17 additions & 0 deletions .changeset/fast-shoes-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@effect/opentelemetry": patch
"effect": patch
---

Add logs to first propagated span, in the following case before this fix the log would not be added to the `p` span because `Effect.fn` adds a fake span for the purpose of adding a stack frame.

```ts
import { Effect } from "effect"

const f = Effect.fn(function* () {
yield* Effect.logWarning("FooBar")
return yield* Effect.fail("Oops")
})

const p = f().pipe(Effect.withSpan("p"))
```
5 changes: 5 additions & 0 deletions .changeset/violet-years-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": minor
---

Fix annotateCurrentSpan, add Effect.currentPropagatedSpan
28 changes: 27 additions & 1 deletion packages/effect/dtslint/Layer.tst.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Layer, Schedule } from "effect"
import { Context, Layer, Schedule } from "effect"
import { describe, expect, it } from "tstyche"

interface In1 {}
Expand All @@ -19,6 +19,8 @@ interface Out3 {}

declare const layer3: Layer.Layer<Out3, Err3, In3>

class TestService1 extends Context.Tag("TestService1")<TestService1, {}>() {}

describe("Layer", () => {
it("merge", () => {
expect(Layer.merge).type.not.toBeCallableWith()
Expand All @@ -40,4 +42,28 @@ describe("Layer", () => {
expect(Layer.retry(layer1, Schedule.recurs(1))).type.toBe<Layer.Layer<Out1, Err1, In1>>()
expect(layer1.pipe(Layer.retry(Schedule.recurs(1)))).type.toBe<Layer.Layer<Out1, Err1, In1>>()
})

it("ensureSuccessType", () => {
expect(layer1.pipe(Layer.ensureSuccessType<Out1>())).type.toBe<Layer.Layer<Out1, Err1, In1>>()
})

it("ensureErrorType", () => {
const withoutError = Layer.succeed(TestService1, {})
expect(withoutError.pipe(Layer.ensureErrorType<never>())).type.toBe<Layer.Layer<TestService1, never, never>>()

const withError = layer1
expect(withError.pipe(Layer.ensureErrorType<Err1>())).type.toBe<Layer.Layer<Out1, Err1, In1>>()
})

it("ensureRequirementsType", () => {
const withoutRequirements = Layer.succeed(TestService1, {})
expect(withoutRequirements.pipe(Layer.ensureRequirementsType<never>())).type.toBe<
Layer.Layer<TestService1, never, never>
>()

const withRequirement = layer1
expect(withRequirement.pipe(Layer.ensureRequirementsType<In1>())).type.toBe<
Layer.Layer<Out1, Err1, In1>
>()
})
})
6 changes: 6 additions & 0 deletions packages/effect/src/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12998,6 +12998,12 @@ export const annotateCurrentSpan: {
*/
export const currentSpan: Effect<Tracer.Span, Cause.NoSuchElementException> = effect.currentSpan

/**
* @since 3.20.0
* @category Tracing
*/
export const currentPropagatedSpan: Effect<Tracer.Span, Cause.NoSuchElementException> = effect.currentPropagatedSpan

/**
* @since 2.0.0
* @category Tracing
Expand Down
52 changes: 52 additions & 0 deletions packages/effect/src/Layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1226,3 +1226,55 @@ export const updateService = dual<
layer,
map(context(), (c) => Context.add(c, tag, f(Context.unsafeGet(c, tag))))
))

// -----------------------------------------------------------------------------
// Type constraints
// -----------------------------------------------------------------------------

/**
* A no-op type constraint that enforces the success channel of a Layer conforms to
* the specified success type `ROut`.
*
* @example
* import { Layer } from "effect"
*
* // Ensure that the layer produces the expected services.
* const program = Layer.succeed(MyService, new MyServiceImpl()).pipe(Layer.ensureSuccessType<MyService>())
*
* @since 3.20.0
* @category Type constraints
*/
export const ensureSuccessType =
<ROut>() => <ROut2 extends ROut, E, RIn>(layer: Layer<ROut2, E, RIn>): Layer<ROut2, E, RIn> => layer

/**
* A no-op type constraint that enforces the error channel of a Layer conforms to
* the specified error type `E`.
*
* @example
* import { Layer } from "effect"
*
* // Ensure that the layer does not expose any unhandled errors.
* const program = Layer.succeed(MyService, new MyServiceImpl()).pipe(Layer.ensureErrorType<never>())
*
* @since 3.20.0
* @category Type constraints
*/
export const ensureErrorType = <E>() => <ROut, E2 extends E, RIn>(layer: Layer<ROut, E2, RIn>): Layer<ROut, E2, RIn> =>
layer

/**
* A no-op type constraint that enforces the requirements channel of a Layer conforms to
* the specified requirements type `RIn`.
*
* @example
* import { Layer } from "effect"
*
* // Ensure that the layer does not have any requirements.
* const program = Layer.succeed(MyService, new MyServiceImpl()).pipe(Layer.ensureRequirementsType<never>())
*
* @since 3.20.0
* @category Type constraints
*/
export const ensureRequirementsType =
<RIn>() => <ROut, E, RIn2 extends RIn>(layer: Layer<ROut, E, RIn2>): Layer<ROut, E, RIn2> => layer
5 changes: 3 additions & 2 deletions packages/effect/src/RcMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export declare namespace RcMap {
*
* - `capacity`: The maximum number of resources that can be held in the map.
* - `idleTimeToLive`: When the reference count reaches zero, the resource will be released after this duration.
* Can be a static duration or a function that returns a duration based on the key.
*
* @since 3.5.0
* @category models
Expand Down Expand Up @@ -85,14 +86,14 @@ export const make: {
<K, A, E, R>(
options: {
readonly lookup: (key: K) => Effect.Effect<A, E, R>
readonly idleTimeToLive?: Duration.DurationInput | undefined
readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined
readonly capacity?: undefined
}
): Effect.Effect<RcMap<K, A, E>, never, Scope.Scope | R>
<K, A, E, R>(
options: {
readonly lookup: (key: K) => Effect.Effect<A, E, R>
readonly idleTimeToLive?: Duration.DurationInput | undefined
readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined
readonly capacity: number
}
): Effect.Effect<RcMap<K, A, E | Cause.ExceededCapacityException>, never, Scope.Scope | R>
Expand Down
25 changes: 18 additions & 7 deletions packages/effect/src/internal/core-effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1966,7 +1966,7 @@ export const annotateCurrentSpan: {
} = function(): Effect.Effect<void> {
const args = arguments
return ignore(core.flatMap(
currentSpan,
currentPropagatedSpan,
(span) =>
core.sync(() => {
if (typeof args[0] === "string") {
Expand Down Expand Up @@ -2041,6 +2041,16 @@ export const currentSpan: Effect.Effect<Tracer.Span, Cause.NoSuchElementExceptio
}
)

export const currentPropagatedSpan: Effect.Effect<Tracer.Span, Cause.NoSuchElementException> = core.flatMap(
core.context<never>(),
(context) => {
const span = filterDisablePropagation(Context.getOption(context, internalTracer.spanTag))
return span._tag === "Some" && span.value._tag === "Span"
? core.succeed(span.value)
: core.fail(new core.NoSuchElementException())
}
)

/* @internal */
export const linkSpans = dual<
(
Expand Down Expand Up @@ -2070,12 +2080,13 @@ export const linkSpans = dual<

const bigint0 = BigInt(0)

const filterDisablePropagation: (self: Option.Option<Tracer.AnySpan>) => Option.Option<Tracer.AnySpan> = Option.flatMap(
(span) =>
Context.get(span.context, internalTracer.DisablePropagation)
? span._tag === "Span" ? filterDisablePropagation(span.parent) : Option.none()
: Option.some(span)
)
export const filterDisablePropagation: (self: Option.Option<Tracer.AnySpan>) => Option.Option<Tracer.AnySpan> = Option
.flatMap(
(span) =>
Context.get(span.context, internalTracer.DisablePropagation)
? span._tag === "Span" ? filterDisablePropagation(span.parent) : Option.none()
: Option.some(span)
)

/** @internal */
export const unsafeMakeSpan = <XA, XE>(
Expand Down
6 changes: 4 additions & 2 deletions packages/effect/src/internal/fiberRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1517,13 +1517,15 @@ export const tracerLogger = globalValue(
logLevel,
message
}) => {
const span = Context.getOption(
const span = internalEffect.filterDisablePropagation(Context.getOption(
fiberRefs.getOrDefault(context, core.currentContext),
tracer.spanTag
)
))

if (span._tag === "None" || span.value._tag === "ExternalSpan") {
return
}

const clockService = Context.unsafeGet(
fiberRefs.getOrDefault(context, defaultServices.currentServices),
clock.clockTag
Expand Down
32 changes: 21 additions & 11 deletions packages/effect/src/internal/rcMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type * as Deferred from "../Deferred.js"
import * as Duration from "../Duration.js"
import type { Effect } from "../Effect.js"
import type { RuntimeFiber } from "../Fiber.js"
import { dual, identity } from "../Function.js"
import { constant, dual, flow, identity } from "../Function.js"
import * as MutableHashMap from "../MutableHashMap.js"
import { pipeArguments } from "../Pipeable.js"
import type * as RcMap from "../RcMap.js"
Expand Down Expand Up @@ -33,6 +33,7 @@ declare namespace State {
readonly deferred: Deferred.Deferred<A, E>
readonly scope: Scope.CloseableScope
readonly finalizer: Effect<void>
readonly idleTimeToLive: Duration.Duration
fiber: RuntimeFiber<void, never> | undefined
expiresAt: number
refCount: number
Expand All @@ -58,7 +59,7 @@ class RcMapImpl<K, A, E> implements RcMap.RcMap<K, A, E> {
readonly lookup: (key: K) => Effect<A, E, Scope.Scope>,
readonly context: Context.Context<never>,
readonly scope: Scope.Scope,
readonly idleTimeToLive: Duration.Duration | undefined,
readonly idleTimeToLive: ((key: K) => Duration.Duration) | undefined,
readonly capacity: number
) {
this[TypeId] = variance
Expand All @@ -73,27 +74,32 @@ class RcMapImpl<K, A, E> implements RcMap.RcMap<K, A, E> {
export const make: {
<K, A, E, R>(options: {
readonly lookup: (key: K) => Effect<A, E, R>
readonly idleTimeToLive?: Duration.DurationInput | undefined
readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined
readonly capacity?: undefined
}): Effect<RcMap.RcMap<K, A, E>, never, Scope.Scope | R>
<K, A, E, R>(options: {
readonly lookup: (key: K) => Effect<A, E, R>
readonly idleTimeToLive?: Duration.DurationInput | undefined
readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined
readonly capacity: number
}): Effect<RcMap.RcMap<K, A, E | Cause.ExceededCapacityException>, never, Scope.Scope | R>
} = <K, A, E, R>(options: {
readonly lookup: (key: K) => Effect<A, E, R>
readonly idleTimeToLive?: Duration.DurationInput | undefined
readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined
readonly capacity?: number | undefined
}) =>
core.withFiberRuntime<RcMap.RcMap<K, A, E>, never, R | Scope.Scope>((fiber) => {
const context = fiber.getFiberRef(core.currentContext) as Context.Context<R | Scope.Scope>
const scope = Context.get(context, fiberRuntime.scopeTag)
const idleTimeToLive = options.idleTimeToLive === undefined
? undefined
: typeof options.idleTimeToLive === "function"
? flow(options.idleTimeToLive, Duration.decode)
: constant(Duration.decode(options.idleTimeToLive))
const self = new RcMapImpl<K, A, E>(
options.lookup as any,
context,
scope,
options.idleTimeToLive ? Duration.decode(options.idleTimeToLive) : undefined,
idleTimeToLive,
Math.max(options.capacity ?? Number.POSITIVE_INFINITY, 0)
)
return core.as(
Expand Down Expand Up @@ -169,10 +175,12 @@ const acquire = core.fnUntraced(function*<K, A, E>(self: RcMapImpl<K, A, E>, key
core.flatMap((exit) => core.deferredDone(deferred, exit)),
circular.forkIn(scope)
)
const idleTimeToLive = self.idleTimeToLive ? self.idleTimeToLive(key) : Duration.zero
const entry: State.Entry<A, E> = {
deferred,
scope,
finalizer: undefined as any,
idleTimeToLive,
fiber: undefined,
expiresAt: 0,
refCount: 1
Expand All @@ -192,19 +200,19 @@ const release = <K, A, E>(self: RcMapImpl<K, A, E>, key: K, entry: State.Entry<A
} else if (
self.state._tag === "Closed"
|| !MutableHashMap.has(self.state.map, key)
|| self.idleTimeToLive === undefined
|| Duration.isZero(entry.idleTimeToLive)
) {
if (self.state._tag === "Open") {
MutableHashMap.remove(self.state.map, key)
}
return core.scopeClose(entry.scope, core.exitVoid)
}

if (!Duration.isFinite(self.idleTimeToLive)) {
if (!Duration.isFinite(entry.idleTimeToLive)) {
return core.void
}

entry.expiresAt = clock.unsafeCurrentTimeMillis() + Duration.toMillis(self.idleTimeToLive)
entry.expiresAt = clock.unsafeCurrentTimeMillis() + Duration.toMillis(entry.idleTimeToLive)
if (entry.fiber) return core.void

return core.interruptibleMask(function loop(restore): Effect<void> {
Expand Down Expand Up @@ -276,10 +284,12 @@ export const touch: {
<K, A, E>(self_: RcMap.RcMap<K, A, E>, key: K) =>
coreEffect.clockWith((clock) => {
const self = self_ as RcMapImpl<K, A, E>
if (!self.idleTimeToLive || self.state._tag === "Closed") return core.void
if (self.state._tag === "Closed") return core.void
const o = MutableHashMap.get(self.state.map, key)
if (o._tag === "None") return core.void
o.value.expiresAt = clock.unsafeCurrentTimeMillis() + Duration.toMillis(self.idleTimeToLive)
const entry = o.value
if (Duration.isZero(entry.idleTimeToLive)) return core.void
entry.expiresAt = clock.unsafeCurrentTimeMillis() + Duration.toMillis(entry.idleTimeToLive)
return core.void
})
)
Loading