Skip to content

Commit

Permalink
add RcMap & RcRef modules (#3179)
Browse files Browse the repository at this point in the history
Co-authored-by: Tim Smart <tim.smart@arisechurch.com>
  • Loading branch information
tim-smart and Tim Smart authored Jul 8, 2024
1 parent 188e23a commit 737701b
Show file tree
Hide file tree
Showing 13 changed files with 947 additions and 0 deletions.
25 changes: 25 additions & 0 deletions .changeset/five-olives-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"effect": minor
---

add RcRef module

An `RcRef` wraps a reference counted resource that can be acquired and released multiple times.

The resource is lazily acquired on the first call to `get` and released when the last reference is released.

```ts
import { Effect, RcRef } from "effect";

Effect.gen(function* () {
const ref = yield* RcRef.make({
acquire: Effect.acquireRelease(Effect.succeed("foo"), () =>
Effect.log("release foo"),
),
});

// will only acquire the resource once, and release it
// when the scope is closed
yield* RcRef.get(ref).pipe(Effect.andThen(RcRef.get(ref)), Effect.scoped);
});
```
5 changes: 5 additions & 0 deletions .changeset/little-taxis-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": minor
---

add Duration.isZero, for checking if a Duration is zero
31 changes: 31 additions & 0 deletions .changeset/spicy-hats-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"effect": minor
---

add RcMap module

An `RcMap` can contain multiple reference counted resources that can be indexed
by a key. The resources are lazily acquired on the first call to `get` and
released when the last reference is released.

Complex keys can extend `Equal` and `Hash` to allow lookups by value.

```ts
import { Effect, RcMap } from "effect";

Effect.gen(function* () {
const map = yield* RcMap.make({
lookup: (key: string) =>
Effect.acquireRelease(Effect.succeed(`acquired ${key}`), () =>
Effect.log(`releasing ${key}`),
),
});

// Get "foo" from the map twice, which will only acquire it once
// It will then be released once the scope closes.
yield* RcMap.get(map, "foo").pipe(
Effect.andThen(RcMap.get(map, "foo")),
Effect.scoped,
);
});
```
44 changes: 44 additions & 0 deletions packages/effect/src/Cause.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ export const InvalidPubSubCapacityExceptionTypeId: unique symbol = core.InvalidP
*/
export type InvalidPubSubCapacityExceptionTypeId = typeof InvalidPubSubCapacityExceptionTypeId

/**
* @since 3.5.0
* @category symbols
*/
export const ExceededCapacityExceptionTypeId: unique symbol = core.ExceededCapacityExceptionTypeId

/**
* @since 3.5.0
* @category symbols
*/
export type ExceededCapacityExceptionTypeId = typeof ExceededCapacityExceptionTypeId

/**
* @since 2.0.0
* @category symbols
Expand Down Expand Up @@ -264,6 +276,18 @@ export interface InvalidPubSubCapacityException extends YieldableError {
readonly [InvalidPubSubCapacityExceptionTypeId]: InvalidPubSubCapacityExceptionTypeId
}

/**
* Represents a checked exception which occurs when a resources capacity has
* been exceeded.
*
* @since 3.5.0
* @category models
*/
export interface ExceededCapacityException extends YieldableError {
readonly _tag: "ExceededCapacityException"
readonly [ExceededCapacityExceptionTypeId]: ExceededCapacityExceptionTypeId
}

/**
* Represents a checked exception which occurs when a computation doesn't
* finish on schedule.
Expand Down Expand Up @@ -907,6 +931,26 @@ export const UnknownException: new(error: unknown, message?: string | undefined)
*/
export const isUnknownException: (u: unknown) => u is UnknownException = core.isUnknownException

/**
* Represents a checked exception which occurs when a resources capacity has
* been exceeded.
*
* @since 3.5.0
* @category errors
*/
export const ExceededCapacityException: new(message?: string | undefined) => ExceededCapacityException =
core.ExceededCapacityException

/**
* Returns `true` if the specified value is an `ExceededCapacityException`, `false`
* otherwise.
*
* @since 3.5.0
* @category refinements
*/
export const isExceededCapacityException: (u: unknown) => u is ExceededCapacityException =
core.isExceededCapacityException

/**
* Returns the specified `Cause` as a pretty-printed string.
*
Expand Down
18 changes: 18 additions & 0 deletions packages/effect/src/Duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,24 @@ export const isDuration = (u: unknown): u is Duration => hasProperty(u, TypeId)
*/
export const isFinite = (self: Duration): boolean => self.value._tag !== "Infinity"

/**
* @since 3.5.0
* @category guards
*/
export const isZero = (self: Duration): boolean => {
switch (self.value._tag) {
case "Millis": {
return self.value.millis === 0
}
case "Nanos": {
return self.value.nanos === bigint0
}
case "Infinity": {
return false
}
}
}

/**
* @since 2.0.0
* @category constructors
Expand Down
103 changes: 103 additions & 0 deletions packages/effect/src/RcMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* @since 3.5.0
*/
import type * as Cause from "./Cause.js"
import type * as Duration from "./Duration.js"
import type * as Effect from "./Effect.js"
import * as internal from "./internal/rcMap.js"
import { type Pipeable } from "./Pipeable.js"
import type * as Scope from "./Scope.js"
import type * as Types from "./Types.js"

/**
* @since 3.5.0
* @category type ids
*/
export const TypeId: unique symbol = internal.TypeId

/**
* @since 3.5.0
* @category type ids
*/
export type TypeId = typeof TypeId

/**
* @since 3.5.0
* @category models
*/
export interface RcMap<in K, out A, out E = never> extends Pipeable {
readonly [TypeId]: RcMap.Variance<K, A, E>
}

/**
* @since 3.5.0
* @category models
*/
export declare namespace RcMap {
/**
* @since 3.5.0
* @category models
*/
export interface Variance<K, A, E> {
readonly _K: Types.Contravariant<K>
readonly _A: Types.Covariant<A>
readonly _E: Types.Covariant<E>
}
}

/**
* An `RcMap` can contain multiple reference counted resources that can be indexed
* by a key. The resources are lazily acquired on the first call to `get` and
* released when the last reference is released.
*
* Complex keys can extend `Equal` and `Hash` to allow lookups by value.
*
* @since 3.5.0
* @category models
* @param capacity The maximum number of resources that can be held in the map.
* @param idleTimeToLive When the reference count reaches zero, the resource will be released after this duration.
* @example
* import { Effect, RcMap } from "effect"
*
* Effect.gen(function*() {
* const map = yield* RcMap.make({
* lookup: (key: string) =>
* Effect.acquireRelease(
* Effect.succeed(`acquired ${key}`),
* () => Effect.log(`releasing ${key}`)
* )
* })
*
* // Get "foo" from the map twice, which will only acquire it once.
* // It will then be released once the scope closes.
* yield* RcMap.get(map, "foo").pipe(
* Effect.andThen(RcMap.get(map, "foo")),
* Effect.scoped
* )
* })
*/
export const make: {
<K, A, E, R>(
options: {
readonly lookup: (key: K) => Effect.Effect<A, E, R>
readonly idleTimeToLive?: 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 capacity: number
}
): Effect.Effect<RcMap<K, A, E | Cause.ExceededCapacityException>, never, Scope.Scope | R>
} = internal.make

/**
* @since 3.5.0
* @category combinators
*/
export const get: {
<K>(key: K): <A, E>(self: RcMap<K, A, E>) => Effect.Effect<A, E, Scope.Scope>
<K, A, E>(self: RcMap<K, A, E>, key: K): Effect.Effect<A, E, Scope.Scope>
} = internal.get
91 changes: 91 additions & 0 deletions packages/effect/src/RcRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* @since 3.5.0
*/
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 Scope from "./Scope.js"
import type * as Types from "./Types.js"

/**
* @since 3.5.0
* @category type ids
*/
export const TypeId: unique symbol = internal.TypeId

/**
* @since 3.5.0
* @category type ids
*/
export type TypeId = typeof TypeId

/**
* @since 3.5.0
* @category models
*/
export interface RcRef<out A, out E = never> extends Pipeable {
readonly [TypeId]: RcRef.Variance<A, E>
}

/**
* @since 3.5.0
* @category models
*/
export declare namespace RcRef {
/**
* @since 3.5.0
* @category models
*/
export interface Variance<A, E> {
readonly _A: Types.Covariant<A>
readonly _E: Types.Covariant<E>
}
}

/**
* Create an `RcRef` from an acquire `Effect`.
*
* An RcRef wraps a reference counted resource that can be acquired and released
* multiple times.
*
* The resource is lazily acquired on the first call to `get` and released when
* the last reference is released.
*
* @since 3.5.0
* @category constructors
* @example
* import { Effect, RcRef } from "effect"
*
* Effect.gen(function*() {
* const ref = yield* RcRef.make({
* acquire: Effect.acquireRelease(
* Effect.succeed("foo"),
* () => Effect.log("release foo")
* )
* })
*
* // will only acquire the resource once, and release it
* // when the scope is closed
* yield* RcRef.get(ref).pipe(
* Effect.andThen(RcRef.get(ref)),
* Effect.scoped
* )
* })
*/
export const make: <A, E, R>(
options: {
readonly acquire: Effect.Effect<A, E, R>
/**
* When the reference count reaches zero, the resource will be released
* after this duration.
*/
readonly idleTimeToLive?: Duration.DurationInput | undefined
}
) => Effect.Effect<RcRef<A, E>, never, R | Scope.Scope> = internal.make

/**
* @since 3.5.0
* @category combinators
*/
export const get: <A, E>(self: RcRef<A, E>) => Effect.Effect<A, E, Scope.Scope> = internal.get
10 changes: 10 additions & 0 deletions packages/effect/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,16 @@ export * as Random from "./Random.js"
*/
export * as RateLimiter from "./RateLimiter.js"

/**
* @since 3.5.0
*/
export * as RcMap from "./RcMap.js"

/**
* @since 3.5.0
*/
export * as RcRef from "./RcRef.js"

/**
* @since 2.0.0
*/
Expand Down
14 changes: 14 additions & 0 deletions packages/effect/src/internal/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2270,6 +2270,20 @@ export const InvalidPubSubCapacityException = makeException<Cause.InvalidPubSubC
[InvalidPubSubCapacityExceptionTypeId]: InvalidPubSubCapacityExceptionTypeId
}, "InvalidPubSubCapacityException")

/** @internal */
export const ExceededCapacityExceptionTypeId: Cause.ExceededCapacityExceptionTypeId = Symbol.for(
"effect/Cause/errors/ExceededCapacityException"
) as Cause.ExceededCapacityExceptionTypeId

/** @internal */
export const ExceededCapacityException = makeException<Cause.ExceededCapacityException>({
[ExceededCapacityExceptionTypeId]: ExceededCapacityExceptionTypeId
}, "ExceededCapacityException")

/** @internal */
export const isExceededCapacityException = (u: unknown): u is Cause.ExceededCapacityException =>
hasProperty(u, ExceededCapacityExceptionTypeId)

/** @internal */
export const isInvalidCapacityError = (u: unknown): u is Cause.InvalidPubSubCapacityException =>
hasProperty(u, InvalidPubSubCapacityExceptionTypeId)
Expand Down
Loading

0 comments on commit 737701b

Please sign in to comment.