From 22a171566400560a0c2fdddbe3b1c3f698e3d53a Mon Sep 17 00:00:00 2001 From: Khraks Mamtsov Date: Sat, 2 Mar 2024 17:44:07 +0100 Subject: [PATCH] `Cache`, `ScopedCache` swap type parameters [another try] (#2239) Co-authored-by: maksim.khramtsov --- .changeset/tough-jeans-allow.md | 7 ++ packages/effect/src/Cache.ts | 22 +++-- packages/effect/src/Request.ts | 2 +- packages/effect/src/ScopedCache.ts | 20 ++-- packages/effect/src/internal/cache.ts | 74 +++++++-------- packages/effect/src/internal/query.ts | 4 +- packages/effect/src/internal/scopedCache.ts | 92 +++++++++---------- packages/effect/test/Cache.test.ts | 2 +- packages/effect/test/ScopedCache.test.ts | 4 +- .../test/utils/cache/WatchableLookup.ts | 36 ++++---- 10 files changed, 137 insertions(+), 126 deletions(-) create mode 100644 .changeset/tough-jeans-allow.md diff --git a/.changeset/tough-jeans-allow.md b/.changeset/tough-jeans-allow.md new file mode 100644 index 0000000000..e047ac747f --- /dev/null +++ b/.changeset/tough-jeans-allow.md @@ -0,0 +1,7 @@ +--- +"effect": minor +--- + +`Cache` has been changed to `Cache`. +`ScopedCache` has been changed to `ScopedCache`. +`Lookup` has been changed to `Lookup` diff --git a/packages/effect/src/Cache.ts b/packages/effect/src/Cache.ts index ff716b0594..6e267ea3e4 100644 --- a/packages/effect/src/Cache.ts +++ b/packages/effect/src/Cache.ts @@ -43,7 +43,7 @@ export type CacheTypeId = typeof CacheTypeId * @since 2.0.0 * @category models */ -export interface Cache extends ConsumerCache { +export interface Cache extends ConsumerCache { /** * Retrieves the value associated with the specified key if it exists. * Otherwise computes the value with the lookup function, puts it in the @@ -83,7 +83,7 @@ export interface Cache extends ConsumerCache extends Cache.Variance { +export interface ConsumerCache extends Cache.Variance { /** * Retrieves the value associated with the specified key if it exists. * Otherwise returns `Option.none`. @@ -156,7 +156,7 @@ export declare namespace Cache { * @since 2.0.0 * @category models */ - export interface Variance { + export interface Variance { readonly [CacheTypeId]: { readonly _Key: Types.Invariant readonly _Error: Types.Covariant @@ -172,13 +172,13 @@ export declare namespace Cache { * @since 2.0.0 * @category constructors */ -export const make: ( +export const make: ( options: { readonly capacity: number readonly timeToLive: Duration.DurationInput - readonly lookup: Lookup + readonly lookup: Lookup } -) => Effect.Effect, never, Environment> = internal.make +) => Effect.Effect, never, Environment> = internal.make /** * Constructs a new cache with the specified capacity, time to live, and @@ -188,13 +188,13 @@ export const make: ( * @since 2.0.0 * @category constructors */ -export const makeWith: ( +export const makeWith: ( options: { readonly capacity: number - readonly lookup: Lookup + readonly lookup: Lookup readonly timeToLive: (exit: Exit.Exit) => Duration.DurationInput } -) => Effect.Effect, never, Environment> = internal.makeWith +) => Effect.Effect, never, Environment> = internal.makeWith /** * `CacheStats` represents a snapshot of statistics for the cache as of a @@ -249,4 +249,6 @@ export const makeEntryStats: (loadedMillis: number) => EntryStats = internal.mak * @since 2.0.0 * @category models */ -export type Lookup = (key: Key) => Effect.Effect +export type Lookup = ( + key: Key +) => Effect.Effect diff --git a/packages/effect/src/Request.ts b/packages/effect/src/Request.ts index 29cf74bf73..ce219bbfb2 100644 --- a/packages/effect/src/Request.ts +++ b/packages/effect/src/Request.ts @@ -257,7 +257,7 @@ export interface Listeners { * @since 2.0.0 */ export interface Cache extends - _Cache.ConsumerCache, never, { + _Cache.ConsumerCache, { listeners: Listeners handle: Deferred }> diff --git a/packages/effect/src/ScopedCache.ts b/packages/effect/src/ScopedCache.ts index e0b65efb18..19cf996b20 100644 --- a/packages/effect/src/ScopedCache.ts +++ b/packages/effect/src/ScopedCache.ts @@ -27,7 +27,9 @@ export type ScopedCacheTypeId = typeof ScopedCacheTypeId * @since 2.0.0 * @category models */ -export interface ScopedCache extends ScopedCache.Variance, Pipeable { +export interface ScopedCache + extends ScopedCache.Variance, Pipeable +{ /** * Retrieves the value associated with the specified key if it exists. * Otherwise returns `Option.none`. @@ -98,7 +100,7 @@ export declare namespace ScopedCache { * @since 2.0.0 * @category models */ - export interface Variance { + export interface Variance { readonly [ScopedCacheTypeId]: { _Key: Types.Contravariant _Error: Types.Covariant @@ -114,13 +116,13 @@ export declare namespace ScopedCache { * @since 2.0.0 * @category constructors */ -export const make: ( +export const make: ( options: { - readonly lookup: Lookup + readonly lookup: Lookup readonly capacity: number readonly timeToLive: Duration.DurationInput } -) => Effect.Effect, never, Scope.Scope | Environment> = internal.make +) => Effect.Effect, never, Scope.Scope | Environment> = internal.make /** * Constructs a new cache with the specified capacity, time to live, and @@ -130,13 +132,13 @@ export const make: ( * @since 2.0.0 * @category constructors */ -export const makeWith: ( +export const makeWith: ( options: { readonly capacity: number - readonly lookup: Lookup + readonly lookup: Lookup readonly timeToLive: (exit: Exit.Exit) => Duration.DurationInput } -) => Effect.Effect, never, Scope.Scope | Environment> = internal.makeWith +) => Effect.Effect, never, Scope.Scope | Environment> = internal.makeWith /** * Similar to `Cache.Lookup`, but executes the lookup function within a `Scope`. @@ -144,6 +146,6 @@ export const makeWith: ( * @since 2.0.0 * @category models */ -export type Lookup = ( +export type Lookup = ( key: Key ) => Effect.Effect diff --git a/packages/effect/src/internal/cache.ts b/packages/effect/src/internal/cache.ts index a19fee4dc1..a54c7feabf 100644 --- a/packages/effect/src/internal/cache.ts +++ b/packages/effect/src/internal/cache.ts @@ -29,13 +29,13 @@ import * as fiberRuntime from "./fiberRuntime.js" * * @internal */ -export type MapValue = - | Complete - | Pending - | Refreshing +export type MapValue = + | Complete + | Pending + | Refreshing /** @internal */ -export interface Complete { +export interface Complete { readonly _tag: "Complete" readonly key: MapKey readonly exit: Exit.Exit @@ -44,26 +44,26 @@ export interface Complete { } /** @internal */ -export interface Pending { +export interface Pending { readonly _tag: "Pending" readonly key: MapKey readonly deferred: Deferred.Deferred } /** @internal */ -export interface Refreshing { +export interface Refreshing { readonly _tag: "Refreshing" readonly deferred: Deferred.Deferred - readonly complete: Complete + readonly complete: Complete } /** @internal */ -export const complete = ( +export const complete = ( key: MapKey, exit: Exit.Exit, entryStats: Cache.EntryStats, timeToLiveMillis: number -): MapValue => +): MapValue => Data.struct({ _tag: "Complete" as const, key, @@ -73,10 +73,10 @@ export const complete = ( }) /** @internal */ -export const pending = ( +export const pending = ( key: MapKey, deferred: Deferred.Deferred -): MapValue => +): MapValue => Data.struct({ _tag: "Pending" as const, key, @@ -84,10 +84,10 @@ export const pending = ( }) /** @internal */ -export const refreshing = ( +export const refreshing = ( deferred: Deferred.Deferred, - complete: Complete -): MapValue => + complete: Complete +): MapValue => Data.struct({ _tag: "Refreshing" as const, deferred, @@ -216,8 +216,8 @@ export const makeKeySet = (): KeySet => new KeySetImpl() * * @internal */ -export interface CacheState { - map: MutableHashMap.MutableHashMap> // mutable by design +export interface CacheState { + map: MutableHashMap.MutableHashMap> // mutable by design keys: KeySet // mutable by design accesses: MutableQueue.MutableQueue> // mutable by design updating: MutableRef.MutableRef // mutable by design @@ -230,14 +230,14 @@ export interface CacheState { * * @internal */ -export const makeCacheState = ( - map: MutableHashMap.MutableHashMap>, +export const makeCacheState = ( + map: MutableHashMap.MutableHashMap>, keys: KeySet, accesses: MutableQueue.MutableQueue>, updating: MutableRef.MutableRef, hits: number, misses: number -): CacheState => ({ +): CacheState => ({ map, keys, accesses, @@ -251,7 +251,7 @@ export const makeCacheState = ( * * @internal */ -export const initialCacheState = (): CacheState => +export const initialCacheState = (): CacheState => makeCacheState( MutableHashMap.empty(), makeKeySet(), @@ -292,14 +292,14 @@ export const makeEntryStats = (loadedMillis: number): Cache.EntryStats => ({ loadedMillis }) -class CacheImpl implements Cache.Cache { +class CacheImpl implements Cache.Cache { readonly [CacheTypeId] = cacheVariance - readonly cacheState: CacheState + readonly cacheState: CacheState constructor( readonly capacity: number, readonly context: Context.Context, readonly fiberId: FiberId.FiberId, - readonly lookup: Cache.Lookup, + readonly lookup: Cache.Lookup, readonly timeToLive: (exit: Exit.Exit) => Duration.DurationInput ) { this.cacheState = initialCacheState() @@ -460,7 +460,7 @@ class CacheImpl implements Cache.Cache { const current = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, k)) if (Equal.equals(current, value)) { - const mapValue = refreshing(deferred, value as Complete) + const mapValue = refreshing(deferred, value as Complete) MutableHashMap.set(this.cacheState.map, k, mapValue) return true } @@ -496,7 +496,7 @@ class CacheImpl implements Cache.Cache + mapValue as Complete ) }) ) @@ -545,7 +545,7 @@ class CacheImpl implements Cache.Cache, + value: MapValue, ignorePending = false ): Effect.Effect, Error> { return effect.clockWith((clock) => { @@ -665,13 +665,13 @@ class CacheImpl implements Cache.Cache( +export const make = ( options: { readonly capacity: number readonly timeToLive: Duration.DurationInput - readonly lookup: Cache.Lookup + readonly lookup: Cache.Lookup } -): Effect.Effect, never, Environment> => { +): Effect.Effect, never, Environment> => { const timeToLive = Duration.decode(options.timeToLive) return makeWith({ capacity: options.capacity, @@ -681,13 +681,13 @@ export const make = ( } /** @internal */ -export const makeWith = ( +export const makeWith = ( options: { readonly capacity: number - readonly lookup: Cache.Lookup + readonly lookup: Cache.Lookup readonly timeToLive: (exit: Exit.Exit) => Duration.DurationInput } -): Effect.Effect, never, Environment> => +): Effect.Effect, never, Environment> => core.map( fiberRuntime.all([core.context(), core.fiberId]), ([context, fiberId]) => @@ -701,12 +701,12 @@ export const makeWith = ( ) /** @internal */ -export const unsafeMakeWith = ( +export const unsafeMakeWith = ( capacity: number, - lookup: Cache.Lookup, + lookup: Cache.Lookup, timeToLive: (exit: Exit.Exit) => Duration.DurationInput -): Cache.Cache => - new CacheImpl( +): Cache.Cache => + new CacheImpl( capacity, Context.empty() as Context.Context, none, diff --git a/packages/effect/src/internal/query.ts b/packages/effect/src/internal/query.ts index 71fcbe8ad2..72506e8015 100644 --- a/packages/effect/src/internal/query.ts +++ b/packages/effect/src/internal/query.ts @@ -12,7 +12,7 @@ import * as core from "./core.js" import { ensuring } from "./fiberRuntime.js" import { Listeners } from "./request.js" -type RequestCache = Cache.Cache, never, { +type RequestCache = Cache.Cache, { listeners: Request.Listeners handle: Deferred }> @@ -21,7 +21,7 @@ type RequestCache = Cache.Cache, never, { export const currentCache = globalValue( Symbol.for("effect/FiberRef/currentCache"), () => - core.fiberRefUnsafeMake(unsafeMakeWith, never, { + core.fiberRefUnsafeMake(unsafeMakeWith, { listeners: Request.Listeners handle: Deferred }>( diff --git a/packages/effect/src/internal/scopedCache.ts b/packages/effect/src/internal/scopedCache.ts index f09ba80c5e..5b2fde02c0 100644 --- a/packages/effect/src/internal/scopedCache.ts +++ b/packages/effect/src/internal/scopedCache.ts @@ -25,8 +25,8 @@ import * as fiberRuntime from "./fiberRuntime.js" * * @internal */ -export interface CacheState { - map: MutableHashMap.MutableHashMap> // mutable by design +export interface CacheState { + map: MutableHashMap.MutableHashMap> // mutable by design keys: _cache.KeySet // mutable by design accesses: MutableQueue.MutableQueue<_cache.MapKey> // mutable by design updating: MutableRef.MutableRef // mutable by design @@ -35,14 +35,14 @@ export interface CacheState { } /** @internal */ -export const makeCacheState = ( - map: MutableHashMap.MutableHashMap>, +export const makeCacheState = ( + map: MutableHashMap.MutableHashMap>, keys: _cache.KeySet, accesses: MutableQueue.MutableQueue<_cache.MapKey>, updating: MutableRef.MutableRef, hits: number, misses: number -): CacheState => ({ +): CacheState => ({ map, keys, accesses, @@ -56,7 +56,7 @@ export const makeCacheState = ( * * @internal */ -export const initialCacheState = (): CacheState => +export const initialCacheState = (): CacheState => makeCacheState( MutableHashMap.empty(), _cache.makeKeySet(), @@ -74,13 +74,13 @@ export const initialCacheState = (): CacheState = - | Complete - | Pending - | Refreshing +export type MapValue = + | Complete + | Pending + | Refreshing /** @internal */ -export interface Complete { +export interface Complete { readonly _tag: "Complete" readonly key: _cache.MapKey readonly exit: Exit.Exit @@ -90,27 +90,27 @@ export interface Complete { } /** @internal */ -export interface Pending { +export interface Pending { readonly _tag: "Pending" readonly key: _cache.MapKey readonly scoped: Effect.Effect> } /** @internal */ -export interface Refreshing { +export interface Refreshing { readonly _tag: "Refreshing" readonly scoped: Effect.Effect> - readonly complete: Complete + readonly complete: Complete } /** @internal */ -export const complete = ( +export const complete = ( key: _cache.MapKey, exit: Exit.Exit, ownerCount: MutableRef.MutableRef, entryStats: Cache.EntryStats, timeToLive: number -): Complete => +): Complete => Data.struct({ _tag: "Complete", key, @@ -121,10 +121,10 @@ export const complete = ( }) /** @internal */ -export const pending = ( +export const pending = ( key: _cache.MapKey, scoped: Effect.Effect> -): Pending => +): Pending => Data.struct({ _tag: "Pending", key, @@ -132,10 +132,10 @@ export const pending = ( }) /** @internal */ -export const refreshing = ( +export const refreshing = ( scoped: Effect.Effect>, - complete: Complete -): Refreshing => + complete: Complete +): Refreshing => Data.struct({ _tag: "Refreshing", scoped, @@ -143,8 +143,8 @@ export const refreshing = ( }) /** @internal */ -export const toScoped = ( - self: Complete +export const toScoped = ( + self: Complete ): Effect.Effect => Exit.matchEffect(self.exit, { onFailure: (cause) => core.failCause(cause), @@ -156,8 +156,8 @@ export const toScoped = ( }) /** @internal */ -export const releaseOwner = ( - self: Complete +export const releaseOwner = ( + self: Complete ): Effect.Effect => Exit.matchEffect(self.exit, { onFailure: () => core.unit, @@ -186,13 +186,13 @@ const scopedCacheVariance = { } class ScopedCacheImpl - implements ScopedCache.ScopedCache + implements ScopedCache.ScopedCache { readonly [ScopedCacheTypeId] = scopedCacheVariance - readonly cacheState: CacheState + readonly cacheState: CacheState constructor( readonly capacity: number, - readonly scopedLookup: ScopedCache.Lookup, + readonly scopedLookup: ScopedCache.Lookup, readonly clock: Clock.Clock, readonly timeToLive: (exit: Exit.Exit) => Duration.Duration, readonly context: Context.Context @@ -285,7 +285,7 @@ class ScopedCacheImpl { - const val = value as Complete + const val = value as Complete const current = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, key)) if (Equal.equals(current, value)) { MutableHashMap.remove(this.cacheState.map, key) @@ -393,7 +393,7 @@ class ScopedCacheImpl, + value: MapValue, ignorePending = false ): Effect.Effect, Error, Scope.Scope>> { switch (value._tag) { @@ -459,14 +459,14 @@ class ScopedCacheImpl( + const completedResult = complete( _cache.makeMapKey(key), exitWithFinalizer, MutableRef.make(1), _cache.makeEntryStats(now), expiredAt ) - let previousValue: MapValue | undefined = undefined + let previousValue: MapValue | undefined = undefined if (MutableHashMap.has(this.cacheState.map, key)) { previousValue = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, key)) } @@ -481,14 +481,14 @@ class ScopedCacheImpl( + const completedResult = complete( _cache.makeMapKey(key), exit as Exit.Exit, MutableRef.make(0), _cache.makeEntryStats(now), expiredAt ) - let previousValue: MapValue | undefined = undefined + let previousValue: MapValue | undefined = undefined if (MutableHashMap.has(this.cacheState.map, key)) { previousValue = Option.getOrUndefined(MutableHashMap.get(this.cacheState.map, key)) } @@ -524,8 +524,8 @@ class ScopedCacheImpl): Array> { - const cleanedKeys: Array> = [] + trackAccess(key: _cache.MapKey): Array> { + const cleanedKeys: Array> = [] MutableQueue.offer(this.cacheState.accesses, key) if (MutableRef.compareAndSet(this.cacheState.updating, false, true)) { let loop = true @@ -558,7 +558,7 @@ class ScopedCacheImpl | undefined): Effect.Effect { + cleanMapValue(mapValue: MapValue | undefined): Effect.Effect { if (mapValue === undefined) { return core.unit } @@ -586,13 +586,13 @@ class ScopedCacheImpl( +export const make = ( options: { - readonly lookup: ScopedCache.Lookup + readonly lookup: ScopedCache.Lookup readonly capacity: number readonly timeToLive: Duration.DurationInput } -): Effect.Effect, never, Environment | Scope.Scope> => { +): Effect.Effect, never, Environment | Scope.Scope> => { const timeToLive = Duration.decode(options.timeToLive) return makeWith({ capacity: options.capacity, @@ -602,13 +602,13 @@ export const make = ( } /** @internal */ -export const makeWith = ( +export const makeWith = ( options: { readonly capacity: number - readonly lookup: ScopedCache.Lookup + readonly lookup: ScopedCache.Lookup readonly timeToLive: (exit: Exit.Exit) => Duration.DurationInput } -): Effect.Effect, never, Environment | Scope.Scope> => +): Effect.Effect, never, Environment | Scope.Scope> => core.flatMap( effect.clock, (clock) => @@ -620,12 +620,12 @@ export const makeWith = ( ) ) -const buildWith = ( +const buildWith = ( capacity: number, - scopedLookup: ScopedCache.Lookup, + scopedLookup: ScopedCache.Lookup, clock: Clock.Clock, timeToLive: (exit: Exit.Exit) => Duration.Duration -): Effect.Effect, never, Environment | Scope.Scope> => +): Effect.Effect, never, Environment | Scope.Scope> => fiberRuntime.acquireRelease( core.flatMap( core.context(), diff --git a/packages/effect/test/Cache.test.ts b/packages/effect/test/Cache.test.ts index 96b65171b1..cfb73aa929 100644 --- a/packages/effect/test/Cache.test.ts +++ b/packages/effect/test/Cache.test.ts @@ -10,7 +10,7 @@ describe("Cache", () => { const cache = yield* _(Cache.make({ capacity: 100, timeToLive: "1 seconds", - lookup: (n: number) => Effect.succeed(n) + lookup: (n: number): Effect.Effect => Effect.succeed(n) })) yield* _(cache.get(42)) yield* _(TestClock.adjust("2 seconds")) diff --git a/packages/effect/test/ScopedCache.test.ts b/packages/effect/test/ScopedCache.test.ts index 8926214d83..f4b1740a28 100644 --- a/packages/effect/test/ScopedCache.test.ts +++ b/packages/effect/test/ScopedCache.test.ts @@ -302,7 +302,7 @@ describe("ScopedCache", () => { it.effect("get - sequential use of a failing scoped effect should cache the error and immediately call the resource finalizer", () => Effect.gen(function*($) { const watchableLookup = yield* $( - WatchableLookup.makeEffect(() => + WatchableLookup.makeEffect(() => Effect.fail(new Cause.RuntimeException("fail")) ) ) @@ -355,7 +355,7 @@ describe("ScopedCache", () => { it.effect("get - concurrent use on a failing scoped effect should cache the error and immediately call the resource finalizer", () => Effect.gen(function*($) { const watchableLookup = yield* $( - WatchableLookup.makeEffect(() => + WatchableLookup.makeEffect(() => Effect.fail(new Cause.RuntimeException("fail")) ) ) diff --git a/packages/effect/test/utils/cache/WatchableLookup.ts b/packages/effect/test/utils/cache/WatchableLookup.ts index 763ae487e6..94569724e6 100644 --- a/packages/effect/test/utils/cache/WatchableLookup.ts +++ b/packages/effect/test/utils/cache/WatchableLookup.ts @@ -11,40 +11,40 @@ import type * as Scope from "effect/Scope" import * as TestServices from "effect/TestServices" import { expect } from "vitest" -export interface WatchableLookup { +export interface WatchableLookup { (key: Key): Effect.Effect - lock(): Effect.Effect - unlock(): Effect.Effect - createdResources(): Effect.Effect< + lock: () => Effect.Effect + unlock: () => Effect.Effect + createdResources: () => Effect.Effect< HashMap.HashMap>> > - firstCreatedResource(key: Key): Effect.Effect> - getCalledTimes(key: Key): Effect.Effect - resourcesCleaned( + firstCreatedResource: (key: Key) => Effect.Effect> + getCalledTimes: (key: Key) => Effect.Effect + resourcesCleaned: ( resources: Iterable> - ): Effect.Effect - assertCalledTimes(key: Key, sizeAssertion: (value: number) => void): Effect.Effect - assertFirstNCreatedResourcesCleaned(key: Key, n: number): Effect.Effect - assertAllCleaned(): Effect.Effect - assertAllCleanedForKey(key: Key): Effect.Effect - assertAtLeastOneResourceNotCleanedForKey(key: Key): Effect.Effect + ) => Effect.Effect + assertCalledTimes: (key: Key, sizeAssertion: (value: number) => void) => Effect.Effect + assertFirstNCreatedResourcesCleaned: (key: Key, n: number) => Effect.Effect + assertAllCleaned: () => Effect.Effect + assertAllCleanedForKey: (key: Key) => Effect.Effect + assertAtLeastOneResourceNotCleanedForKey: (key: Key) => Effect.Effect } export const make = ( concreteLookup: (key: Key) => Value -): Effect.Effect> => makeEffect((key) => Effect.succeed(concreteLookup(key))) +): Effect.Effect> => makeEffect((key) => Effect.succeed(concreteLookup(key))) -export const makeUnit = (): Effect.Effect> => make((_: void) => void 0) +export const makeUnit = (): Effect.Effect> => make((_: void) => void 0) -export const makeEffect = ( +export const makeEffect = ( concreteLookup: (key: Key) => Effect.Effect -): Effect.Effect> => +): Effect.Effect> => Effect.map( Effect.zip( Ref.make(false), Ref.make(HashMap.empty>>()) ), - ([blocked, resources]): WatchableLookup => { + ([blocked, resources]): WatchableLookup => { function lookup(key: Key): Effect.Effect { return Effect.flatten(Effect.gen(function*($) { const observableResource = yield* $(ObservableResource.makeEffect(concreteLookup(key)))