| /** | |
| * Caches values that need scoped resource management. | |
| * | |
| * Each cached entry owns its own `Scope`, so resources opened while creating a | |
| * value stay alive while that entry is cached and are released when the entry is | |
| * removed. A `ScopedCache` also belongs to an outer scope, which closes all | |
| * remaining entries when the cache is closed. Lookups for the same missing key | |
| * share one in-progress effect, and entries can expire, be refreshed, be | |
| * invalidated, or be evicted by capacity limits. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Arr from "./Array.ts" | |
| import * as Context from "./Context.ts" | |
| import * as Deferred from "./Deferred.ts" | |
| import * as Duration from "./Duration.ts" | |
| import type * as Effect from "./Effect.ts" | |
| import type * as Exit from "./Exit.ts" | |
| import * as Fiber from "./Fiber.ts" | |
| import { dual, identity } from "./Function.ts" | |
| import * as core from "./internal/core.ts" | |
| import { PipeInspectableProto } from "./internal/core.ts" | |
| import * as effect from "./internal/effect.ts" | |
| import * as MutableHashMap from "./MutableHashMap.ts" | |
| import * as Option from "./Option.ts" | |
| import type { Pipeable } from "./Pipeable.ts" | |
| import * as Predicate from "./Predicate.ts" | |
| import * as Scope from "./Scope.ts" | |
| const TypeId = "~effect/ScopedCache" | |
| /** | |
| * A scoped cache whose values are acquired by a lookup effect and stored in | |
| * per-entry scopes. | |
| * | |
| * **When to use** | |
| * | |
| * Use to cache values that acquire scoped resources and must release those | |
| * resources when entries expire, are evicted, or are invalidated. | |
| * | |
| * **Details** | |
| * | |
| * Concurrent requests for the same key share the same in-flight lookup. | |
| * Entries can expire based on the lookup exit, are evicted when capacity is | |
| * exceeded, and release their entry scopes when invalidated, evicted, expired, | |
| * or when the cache's owning scope closes. | |
| * | |
| * @see {@link make} for creating a scoped cache with a fixed time-to-live | |
| * @see {@link makeWith} for creating a scoped cache with dynamic time-to-live | |
| * | |
| * @category models | |
| * @since 2.0.0 | |
| */ | |
| export interface ScopedCache<in out Key, in out A, in out E = never, out R = never> extends Pipeable { | |
| readonly [TypeId]: typeof TypeId | |
| state: State<Key, A, E> | |
| readonly capacity: number | |
| readonly lookup: (key: Key) => Effect.Effect<A, E, R | Scope.Scope> | |
| readonly timeToLive: (exit: Exit.Exit<A, E>, key: Key) => Duration.Duration | |
| } | |
| /** | |
| * Represents whether a `ScopedCache` is open or closed. | |
| * | |
| * **When to use** | |
| * | |
| * Use when inspecting the low-level lifecycle state of a scoped cache. | |
| * | |
| * **Details** | |
| * | |
| * `Open` stores cached entries in access order for reuse and eviction. | |
| * `Closed` means the owning scope has closed and the cache can no longer | |
| * perform lookup operations. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type State<K, A, E> = { | |
| readonly _tag: "Open" | |
| readonly map: MutableHashMap.MutableHashMap<K, Entry<A, E>> | |
| } | { | |
| readonly _tag: "Closed" | |
| } | |
| /** | |
| * A single scoped cache entry. | |
| * | |
| * **When to use** | |
| * | |
| * Use when inspecting the open state of a `ScopedCache` and you need the stored | |
| * deferred result, entry scope, or expiration timestamp for a key. | |
| * | |
| * **Details** | |
| * | |
| * The entry contains the deferred lookup result shared by readers, the scope | |
| * that owns resources acquired while computing the value, and an optional | |
| * expiration time in milliseconds. Removing the entry closes its scope. | |
| * | |
| * @see {@link State} for the open/closed cache state that stores entries by key | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Entry<A, E> { | |
| expiresAt: number | undefined | |
| readonly deferred: Deferred.Deferred<A, E> | |
| readonly scope: Scope.Closeable | |
| } | |
| /** | |
| * Creates a `ScopedCache` from a lookup function, maximum capacity, and a | |
| * time-to-live function computed from each lookup exit and key. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you need a scoped cache whose entry lifetime depends on each lookup | |
| * result or key. | |
| * | |
| * **Details** | |
| * | |
| * The cache must be constructed in a `Scope`. Each lookup runs in its own entry | |
| * scope, and that scope is closed when the entry expires, is invalidated, is | |
| * evicted by capacity, or when the cache's owning scope closes. | |
| * `requireServicesAt` controls whether lookup services are captured at | |
| * construction time or required when lookup operations run. | |
| * | |
| * @see {@link make} for creating a scoped cache with one fixed time-to-live | |
| * | |
| * @category constructors | |
| * @since 2.0.0 | |
| */ | |
| export const makeWith = < | |
| Key, | |
| A, | |
| E = never, | |
| R = never, | |
| ServiceMode extends "lookup" | "construction" = never | |
| >(options: { | |
| readonly lookup: (key: Key) => Effect.Effect<A, E, R | Scope.Scope> | |
| readonly capacity: number | |
| readonly timeToLive?: ((exit: Exit.Exit<A, E>, key: Key) => Duration.Input) | undefined | |
| readonly requireServicesAt?: ServiceMode | undefined | |
| }): Effect.Effect< | |
| ScopedCache<Key, A, E, "lookup" extends ServiceMode ? Exclude<R, Scope.Scope> : never>, | |
| never, | |
| ("lookup" extends ServiceMode ? never : R) | Scope.Scope | |
| > => | |
| effect.contextWith((context: Context.Context<any>) => { | |
| const scope = Context.get(context, Scope.Scope) | |
| const self = Object.create(Proto) | |
| self.lookup = (key: Key): Effect.Effect<A, E> => | |
| effect.updateContext( | |
| options.lookup(key), | |
| (input) => Context.merge(context, input) | |
| ) | |
| const map = MutableHashMap.empty<Key, Entry<A, E>>() | |
| self.state = { _tag: "Open", map } | |
| self.capacity = options.capacity | |
| self.timeToLive = options.timeToLive | |
| ? (exit: Exit.Exit<A, E>, key: Key) => Duration.fromInputUnsafe(options.timeToLive!(exit, key)) | |
| : defaultTimeToLive | |
| return effect.as( | |
| Scope.addFinalizer( | |
| scope, | |
| core.withFiber((fiber) => { | |
| self.state = { _tag: "Closed" } | |
| return invalidateAllImpl(fiber, map) | |
| }) | |
| ), | |
| self | |
| ) | |
| }) | |
| /** | |
| * Creates a `ScopedCache` with a fixed time-to-live for every lookup result. | |
| * | |
| * **When to use** | |
| * | |
| * Use to create a scoped cache when every cached lookup result should share the | |
| * same lifetime. | |
| * | |
| * **Details** | |
| * | |
| * This is the constant-TTL variant of `makeWith`: values are acquired by the | |
| * lookup effect in per-entry scopes, capacity can evict older entries, and | |
| * entry scopes are closed when entries expire, are invalidated, are evicted, or | |
| * when the cache's owning scope closes. | |
| * | |
| * @see {@link makeWith} for computing time-to-live from each lookup result and key | |
| * | |
| * @category constructors | |
| * @since 2.0.0 | |
| */ | |
| export const make = < | |
| Key, | |
| A, | |
| E = never, | |
| R = never, | |
| ServiceMode extends "lookup" | "construction" = never | |
| >( | |
| options: { | |
| readonly lookup: (key: Key) => Effect.Effect<A, E, R | Scope.Scope> | |
| readonly capacity: number | |
| readonly timeToLive?: Duration.Input | undefined | |
| readonly requireServicesAt?: ServiceMode | undefined | |
| } | |
| ): Effect.Effect< | |
| ScopedCache<Key, A, E, "lookup" extends ServiceMode ? Exclude<R, Scope.Scope> : never>, | |
| never, | |
| ("lookup" extends ServiceMode ? never : R) | Scope.Scope | |
| > => | |
| makeWith<Key, A, E, R, ServiceMode>({ | |
| ...options, | |
| timeToLive: options.timeToLive ? () => options.timeToLive! : defaultTimeToLive | |
| }) | |
| const Proto = { | |
| ...PipeInspectableProto, | |
| [TypeId]: TypeId, | |
| toJSON(this: ScopedCache<any, any, any>) { | |
| return { | |
| _id: "ScopedCache", | |
| capacity: this.capacity, | |
| state: this.state | |
| } | |
| } | |
| } | |
| const defaultTimeToLive = <A, E>(_: Exit.Exit<A, E>, _key: unknown): Duration.Duration => Duration.infinity | |
| /** | |
| * Gets the value for a key, running the cache lookup when no unexpired entry is | |
| * present. | |
| * | |
| * **When to use** | |
| * | |
| * Use to retrieve a scoped cached value by key when a missing or expired entry | |
| * should run the cache lookup and share the in-flight lookup with concurrent | |
| * callers. | |
| * | |
| * **Details** | |
| * | |
| * Concurrent `get` calls for the same key share the same in-flight lookup. | |
| * Successful and failed lookup exits are cached according to the configured | |
| * TTL. If the cache is closed, the effect is interrupted. | |
| * | |
| * @see {@link getOption} for reading only when an unexpired entry is already cached | |
| * @see {@link getSuccess} for inspecting an already-completed successful entry | |
| * @see {@link refresh} for forcing a new lookup | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const get: { | |
| /** | |
| * Gets the value for a key, running the cache lookup when no unexpired entry is | |
| * present. | |
| * | |
| * **When to use** | |
| * | |
| * Use to retrieve a scoped cached value by key when a missing or expired entry | |
| * should run the cache lookup and share the in-flight lookup with concurrent | |
| * callers. | |
| * | |
| * **Details** | |
| * | |
| * Concurrent `get` calls for the same key share the same in-flight lookup. | |
| * Successful and failed lookup exits are cached according to the configured | |
| * TTL. If the cache is closed, the effect is interrupted. | |
| * | |
| * @see {@link getOption} for reading only when an unexpired entry is already cached | |
| * @see {@link getSuccess} for inspecting an already-completed successful entry | |
| * @see {@link refresh} for forcing a new lookup | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A>(key: Key): <E, R>(self: ScopedCache<Key, A, E, R>) => Effect.Effect<A, E, R> | |
| /** | |
| * Gets the value for a key, running the cache lookup when no unexpired entry is | |
| * present. | |
| * | |
| * **When to use** | |
| * | |
| * Use to retrieve a scoped cached value by key when a missing or expired entry | |
| * should run the cache lookup and share the in-flight lookup with concurrent | |
| * callers. | |
| * | |
| * **Details** | |
| * | |
| * Concurrent `get` calls for the same key share the same in-flight lookup. | |
| * Successful and failed lookup exits are cached according to the configured | |
| * TTL. If the cache is closed, the effect is interrupted. | |
| * | |
| * @see {@link getOption} for reading only when an unexpired entry is already cached | |
| * @see {@link getSuccess} for inspecting an already-completed successful entry | |
| * @see {@link refresh} for forcing a new lookup | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key): Effect.Effect<A, E, R> | |
| } = dual( | |
| 2, | |
| <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key): Effect.Effect<A, E, R> => | |
| effect.uninterruptibleMask((restore) => | |
| core.withFiber((fiber) => { | |
| const state = self.state | |
| if (state._tag === "Closed") { | |
| return effect.interrupt | |
| } | |
| const oentry = MutableHashMap.get(state.map, key) | |
| if (Option.isSome(oentry) && !hasExpired(oentry.value, fiber)) { | |
| // Move the entry to the end of the map to keep it fresh | |
| MutableHashMap.remove(state.map, key) | |
| MutableHashMap.set(state.map, key, oentry.value) | |
| return restore(Deferred.await(oentry.value.deferred)) | |
| } | |
| const scope = Scope.makeUnsafe() | |
| const deferred = Deferred.makeUnsafe</** | |
| * Gets the value for a key, running the cache lookup when no unexpired entry is | |
| * present. | |
| * | |
| * **When to use** | |
| * | |
| * Use to retrieve a scoped cached value by key when a missing or expired entry | |
| * should run the cache lookup and share the in-flight lookup with concurrent | |
| * callers. | |
| * | |
| * **Details** | |
| * | |
| * Concurrent `get` calls for the same key share the same in-flight lookup. | |
| * Successful and failed lookup exits are cached according to the configured | |
| * TTL. If the cache is closed, the effect is interrupted. | |
| * | |
| * @see {@link getOption} for reading only when an unexpired entry is already cached | |
| * @see {@link getSuccess} for inspecting an already-completed successful entry | |
| * @see {@link refresh} for forcing a new lookup | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| A, /** | |
| * Gets the value for a key, running the cache lookup when no unexpired entry is | |
| * present. | |
| * | |
| * **When to use** | |
| * | |
| * Use to retrieve a scoped cached value by key when a missing or expired entry | |
| * should run the cache lookup and share the in-flight lookup with concurrent | |
| * callers. | |
| * | |
| * **Details** | |
| * | |
| * Concurrent `get` calls for the same key share the same in-flight lookup. | |
| * Successful and failed lookup exits are cached according to the configured | |
| * TTL. If the cache is closed, the effect is interrupted. | |
| * | |
| * @see {@link getOption} for reading only when an unexpired entry is already cached | |
| * @see {@link getSuccess} for inspecting an already-completed successful entry | |
| * @see {@link refresh} for forcing a new lookup | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| E>() | |
| const entry: Entry<A, E> = { | |
| expiresAt: undefined, | |
| deferred, | |
| scope | |
| } | |
| MutableHashMap.set(state.map, key, entry) | |
| return checkCapacity(fiber, state.map, self.capacity).pipe( | |
| Option.isSome(oentry) ? effect.flatMap(() => Scope.close(oentry.value.scope, effect.exitVoid)) : identity, | |
| effect.flatMap(() => Scope.provide(restore(self.lookup(key)), scope)), | |
| effect.onExit((exit) => { | |
| Deferred.doneUnsafe(deferred, exit) | |
| const ttl = self.timeToLive(exit, key) | |
| if (Duration.isFinite(ttl)) { | |
| entry.expiresAt = fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + Duration.toMillis(ttl) | |
| } | |
| return effect.void | |
| }) | |
| ) | |
| }) | |
| ) | |
| ) | |
| const hasExpired = <A, E>(entry: Entry<A, E>, fiber: Fiber.Fiber<unknown, unknown>): boolean => { | |
| if (entry.expiresAt === undefined) { | |
| return false | |
| } | |
| return fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() >= entry.expiresAt | |
| } | |
| const checkCapacity = <K, A, E>( | |
| parent: Fiber.Fiber<unknown, unknown>, | |
| map: MutableHashMap.MutableHashMap<K, Entry<A, E>>, | |
| capacity: number | |
| ): Effect.Effect<void> => { | |
| if (!Number.isFinite(capacity)) return effect.void | |
| let diff = MutableHashMap.size(map) - capacity | |
| if (diff <= 0) return effect.void | |
| // MutableHashMap has insertion order, so we can remove the oldest entries | |
| const fibers = Arr.empty<Fiber.Fiber<unknown, unknown>>() | |
| for (const [key, entry] of map) { | |
| MutableHashMap.remove(map, key) | |
| fibers.push(effect.forkUnsafe(parent as any, Scope.close(entry.scope, effect.exitVoid), true)) | |
| diff-- | |
| if (diff === 0) break | |
| } | |
| return effect.fiberAwaitAll(fibers) | |
| } | |
| /** | |
| * Reads an existing unexpired cache entry without running the lookup function. | |
| * | |
| * **When to use** | |
| * | |
| * Use to read a scoped value only when it is already cached, without starting | |
| * the lookup for missing or expired keys. | |
| * | |
| * **Details** | |
| * | |
| * Returns `Option.none` when the key is absent or expired. If an entry exists, | |
| * the effect waits for its cached result and returns `Option.some(value)` on | |
| * success, or fails with the cached lookup error. | |
| * | |
| * @see {@link get} for running the lookup on missing or expired keys | |
| * @see {@link getSuccess} for inspecting only already-completed successful entries | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const getOption: { | |
| /** | |
| * Reads an existing unexpired cache entry without running the lookup function. | |
| * | |
| * **When to use** | |
| * | |
| * Use to read a scoped value only when it is already cached, without starting | |
| * the lookup for missing or expired keys. | |
| * | |
| * **Details** | |
| * | |
| * Returns `Option.none` when the key is absent or expired. If an entry exists, | |
| * the effect waits for its cached result and returns `Option.some(value)` on | |
| * success, or fails with the cached lookup error. | |
| * | |
| * @see {@link get} for running the lookup on missing or expired keys | |
| * @see {@link getSuccess} for inspecting only already-completed successful entries | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A>(key: Key): <E, R>(self: ScopedCache<Key, A, E, R>) => Effect.Effect<Option.Option<A>, E> | |
| /** | |
| * Reads an existing unexpired cache entry without running the lookup function. | |
| * | |
| * **When to use** | |
| * | |
| * Use to read a scoped value only when it is already cached, without starting | |
| * the lookup for missing or expired keys. | |
| * | |
| * **Details** | |
| * | |
| * Returns `Option.none` when the key is absent or expired. If an entry exists, | |
| * the effect waits for its cached result and returns `Option.some(value)` on | |
| * success, or fails with the cached lookup error. | |
| * | |
| * @see {@link get} for running the lookup on missing or expired keys | |
| * @see {@link getSuccess} for inspecting only already-completed successful entries | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key): Effect.Effect<Option.Option<A>, E> | |
| } = dual( | |
| 2, | |
| <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key): Effect.Effect<Option.Option<A>, E> => | |
| effect.uninterruptibleMask((restore) => | |
| core.withFiber((fiber) => | |
| effect.flatMap( | |
| getImpl(self, key, fiber), | |
| (entry) => entry ? effect.asSome(restore(Deferred.await(entry.deferred))) : effect.succeedNone | |
| ) | |
| ) | |
| ) | |
| ) | |
| const getImpl = <Key, A, E, R>( | |
| self: ScopedCache<Key, A, E, R>, | |
| key: Key, | |
| fiber: Fiber.Fiber<any, any>, | |
| isRead = true | |
| ): Effect.Effect<Entry<A, E> | undefined> => { | |
| if (self.state._tag === "Closed") { | |
| return effect.interrupt | |
| } | |
| const state = self.state | |
| const oentry = MutableHashMap.get(state.map, key) | |
| if (Option.isNone(oentry)) { | |
| return effect.undefined | |
| } else if (hasExpired(oentry.value, fiber)) { | |
| MutableHashMap.remove(state.map, key) | |
| return effect.as( | |
| Scope.close(oentry.value.scope, effect.exitVoid), | |
| undefined | |
| ) | |
| } else if (isRead) { | |
| MutableHashMap.remove(state.map, key) | |
| MutableHashMap.set(state.map, key, oentry.value) | |
| } | |
| return effect.succeed(oentry.value) | |
| } | |
| /** | |
| * Retrieves the value associated with the specified key from the cache, only if | |
| * it contains a resolved successful value. | |
| * | |
| * **When to use** | |
| * | |
| * Use to inspect an already-completed successful scoped cache entry without | |
| * running or awaiting the lookup effect. | |
| * | |
| * **Details** | |
| * | |
| * Returns `Option.some` for a resolved successful entry. Returns `Option.none` | |
| * for missing, expired, failed, or still-pending entries. | |
| * | |
| * @see {@link get} for awaiting or starting the lookup effect | |
| * @see {@link getOption} for awaiting an already-cached entry without starting a lookup | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const getSuccess: { | |
| /** | |
| * Retrieves the value associated with the specified key from the cache, only if | |
| * it contains a resolved successful value. | |
| * | |
| * **When to use** | |
| * | |
| * Use to inspect an already-completed successful scoped cache entry without | |
| * running or awaiting the lookup effect. | |
| * | |
| * **Details** | |
| * | |
| * Returns `Option.some` for a resolved successful entry. Returns `Option.none` | |
| * for missing, expired, failed, or still-pending entries. | |
| * | |
| * @see {@link get} for awaiting or starting the lookup effect | |
| * @see {@link getOption} for awaiting an already-cached entry without starting a lookup | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A, R>(key: Key): <E>(self: ScopedCache<Key, A, E, R>) => Effect.Effect<Option.Option<A>> | |
| /** | |
| * Retrieves the value associated with the specified key from the cache, only if | |
| * it contains a resolved successful value. | |
| * | |
| * **When to use** | |
| * | |
| * Use to inspect an already-completed successful scoped cache entry without | |
| * running or awaiting the lookup effect. | |
| * | |
| * **Details** | |
| * | |
| * Returns `Option.some` for a resolved successful entry. Returns `Option.none` | |
| * for missing, expired, failed, or still-pending entries. | |
| * | |
| * @see {@link get} for awaiting or starting the lookup effect | |
| * @see {@link getOption} for awaiting an already-cached entry without starting a lookup | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key): Effect.Effect<Option.Option<A>> | |
| } = dual( | |
| 2, | |
| <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key): Effect.Effect<Option.Option<A>> => | |
| effect.uninterruptible( | |
| core.withFiber((fiber) => | |
| effect.map( | |
| getImpl(self, key, fiber), | |
| (entry) => { | |
| const exit = entry?.deferred.effect as Exit.Exit<A, E> | undefined | |
| if (exit && effect.exitIsSuccess(exit)) { | |
| return Option.some(exit.value) | |
| } | |
| return Option.none() | |
| } | |
| ) | |
| ) | |
| ) | |
| ) | |
| /** | |
| * Sets a successful value for a key without running the lookup function. | |
| * | |
| * **When to use** | |
| * | |
| * Use to seed or overwrite a scoped cache entry with an already available | |
| * successful value. | |
| * | |
| * **Details** | |
| * | |
| * This replaces and closes any existing entry scope for the key, applies the | |
| * cache's TTL using a successful exit for the value, and may evict older | |
| * entries if the cache capacity is exceeded. | |
| * | |
| * @see {@link get} for reading or computing a cached value | |
| * @see {@link refresh} for replacing an entry by running the lookup function | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const set: { | |
| /** | |
| * Sets a successful value for a key without running the lookup function. | |
| * | |
| * **When to use** | |
| * | |
| * Use to seed or overwrite a scoped cache entry with an already available | |
| * successful value. | |
| * | |
| * **Details** | |
| * | |
| * This replaces and closes any existing entry scope for the key, applies the | |
| * cache's TTL using a successful exit for the value, and may evict older | |
| * entries if the cache capacity is exceeded. | |
| * | |
| * @see {@link get} for reading or computing a cached value | |
| * @see {@link refresh} for replacing an entry by running the lookup function | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A>(key: Key, value: A): <E, R>(self: ScopedCache<Key, A, E, R>) => Effect.Effect<void> | |
| /** | |
| * Sets a successful value for a key without running the lookup function. | |
| * | |
| * **When to use** | |
| * | |
| * Use to seed or overwrite a scoped cache entry with an already available | |
| * successful value. | |
| * | |
| * **Details** | |
| * | |
| * This replaces and closes any existing entry scope for the key, applies the | |
| * cache's TTL using a successful exit for the value, and may evict older | |
| * entries if the cache capacity is exceeded. | |
| * | |
| * @see {@link get} for reading or computing a cached value | |
| * @see {@link refresh} for replacing an entry by running the lookup function | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key, value: A): Effect.Effect<void> | |
| } = dual( | |
| 3, | |
| <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key, value: A): Effect.Effect<void> => | |
| effect.uninterruptible( | |
| core.withFiber((fiber) => { | |
| if (self.state._tag === "Closed") { | |
| return effect.interrupt | |
| } | |
| const oentry = MutableHashMap.get(self.state.map, key) | |
| const state = self.state | |
| const exit = core.exitSucceed(value) | |
| const deferred = Deferred.makeUnsafe</** | |
| * Sets a successful value for a key without running the lookup function. | |
| * | |
| * **When to use** | |
| * | |
| * Use to seed or overwrite a scoped cache entry with an already available | |
| * successful value. | |
| * | |
| * **Details** | |
| * | |
| * This replaces and closes any existing entry scope for the key, applies the | |
| * cache's TTL using a successful exit for the value, and may evict older | |
| * entries if the cache capacity is exceeded. | |
| * | |
| * @see {@link get} for reading or computing a cached value | |
| * @see {@link refresh} for replacing an entry by running the lookup function | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| A, /** | |
| * Sets a successful value for a key without running the lookup function. | |
| * | |
| * **When to use** | |
| * | |
| * Use to seed or overwrite a scoped cache entry with an already available | |
| * successful value. | |
| * | |
| * **Details** | |
| * | |
| * This replaces and closes any existing entry scope for the key, applies the | |
| * cache's TTL using a successful exit for the value, and may evict older | |
| * entries if the cache capacity is exceeded. | |
| * | |
| * @see {@link get} for reading or computing a cached value | |
| * @see {@link refresh} for replacing an entry by running the lookup function | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| E>() | |
| Deferred.doneUnsafe(deferred, exit) | |
| const ttl = self.timeToLive(exit, key) | |
| MutableHashMap.set(state.map, key, { | |
| scope: Scope.makeUnsafe(), | |
| deferred, | |
| expiresAt: Duration.isFinite(ttl) | |
| ? fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + Duration.toMillis(ttl) | |
| : undefined | |
| }) | |
| const check = checkCapacity(fiber, state.map, self.capacity) | |
| return Option.isSome(oentry) | |
| ? effect.flatMap(Scope.close(oentry.value.scope, effect.exitVoid), () => check) | |
| : check | |
| }) | |
| ) | |
| ) | |
| /** | |
| * Checks whether the cache contains an entry for the specified key. | |
| * | |
| * **When to use** | |
| * | |
| * Use to test whether an unexpired entry exists for a key without running the | |
| * cache lookup. | |
| * | |
| * **Details** | |
| * | |
| * This does not start lookups and does not refresh access order. Expired | |
| * entries are treated as absent and their scopes are closed while checking. If | |
| * the cache is closed, the effect is interrupted. | |
| * | |
| * @see {@link getOption} for reading an existing cached entry | |
| * @see {@link get} for running the lookup on missing or expired keys | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const has: { | |
| /** | |
| * Checks whether the cache contains an entry for the specified key. | |
| * | |
| * **When to use** | |
| * | |
| * Use to test whether an unexpired entry exists for a key without running the | |
| * cache lookup. | |
| * | |
| * **Details** | |
| * | |
| * This does not start lookups and does not refresh access order. Expired | |
| * entries are treated as absent and their scopes are closed while checking. If | |
| * the cache is closed, the effect is interrupted. | |
| * | |
| * @see {@link getOption} for reading an existing cached entry | |
| * @see {@link get} for running the lookup on missing or expired keys | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A>(key: Key): <E, R>(self: ScopedCache<Key, A, E, R>) => Effect.Effect<boolean> | |
| /** | |
| * Checks whether the cache contains an entry for the specified key. | |
| * | |
| * **When to use** | |
| * | |
| * Use to test whether an unexpired entry exists for a key without running the | |
| * cache lookup. | |
| * | |
| * **Details** | |
| * | |
| * This does not start lookups and does not refresh access order. Expired | |
| * entries are treated as absent and their scopes are closed while checking. If | |
| * the cache is closed, the effect is interrupted. | |
| * | |
| * @see {@link getOption} for reading an existing cached entry | |
| * @see {@link get} for running the lookup on missing or expired keys | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key): Effect.Effect<boolean> | |
| } = dual( | |
| 2, | |
| <Key, A, E>(self: ScopedCache<Key, A, E>, key: Key): Effect.Effect<boolean> => | |
| effect.uninterruptible( | |
| core.withFiber((fiber) => effect.map(getImpl(self, key, fiber, false), Predicate.isNotUndefined)) | |
| ) | |
| ) | |
| /** | |
| * Removes the entry associated with a key and closes its entry scope. | |
| * | |
| * **When to use** | |
| * | |
| * Use to remove a single key from a scoped cache and release any resources owned | |
| * by that entry before a later lookup computes it again. | |
| * | |
| * **Details** | |
| * | |
| * If the key is absent, this is a no-op. | |
| * | |
| * **Gotchas** | |
| * | |
| * If the cache is closed, the effect is interrupted. | |
| * | |
| * @see {@link refresh} for replacing a key by running a new lookup immediately | |
| * @see {@link invalidateWhen} for invalidating only when a cached value matches a predicate | |
| * @see {@link invalidateAll} for removing every cached entry | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const invalidate: { | |
| /** | |
| * Removes the entry associated with a key and closes its entry scope. | |
| * | |
| * **When to use** | |
| * | |
| * Use to remove a single key from a scoped cache and release any resources owned | |
| * by that entry before a later lookup computes it again. | |
| * | |
| * **Details** | |
| * | |
| * If the key is absent, this is a no-op. | |
| * | |
| * **Gotchas** | |
| * | |
| * If the cache is closed, the effect is interrupted. | |
| * | |
| * @see {@link refresh} for replacing a key by running a new lookup immediately | |
| * @see {@link invalidateWhen} for invalidating only when a cached value matches a predicate | |
| * @see {@link invalidateAll} for removing every cached entry | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A>(key: Key): <E, R>(self: ScopedCache<Key, A, E, R>) => Effect.Effect<void> | |
| /** | |
| * Removes the entry associated with a key and closes its entry scope. | |
| * | |
| * **When to use** | |
| * | |
| * Use to remove a single key from a scoped cache and release any resources owned | |
| * by that entry before a later lookup computes it again. | |
| * | |
| * **Details** | |
| * | |
| * If the key is absent, this is a no-op. | |
| * | |
| * **Gotchas** | |
| * | |
| * If the cache is closed, the effect is interrupted. | |
| * | |
| * @see {@link refresh} for replacing a key by running a new lookup immediately | |
| * @see {@link invalidateWhen} for invalidating only when a cached value matches a predicate | |
| * @see {@link invalidateAll} for removing every cached entry | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key): Effect.Effect<void> | |
| } = dual(2, <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key): Effect.Effect<void> => | |
| effect.uninterruptible( | |
| effect.suspend(() => { | |
| if (self.state._tag === "Closed") { | |
| return effect.interrupt | |
| } | |
| const oentry = MutableHashMap.get(self.state.map, key) | |
| if (Option.isNone(oentry)) { | |
| return effect.void | |
| } | |
| MutableHashMap.remove(self.state.map, key) | |
| return Scope.close(oentry.value.scope, effect.exitVoid) | |
| }) | |
| )) | |
| /** | |
| * Invalidates the entry associated with the specified key in the cache when the | |
| * predicate returns true for the cached value. | |
| * | |
| * **When to use** | |
| * | |
| * Use to remove an already-cached scoped value only when the successful cached | |
| * value satisfies a predicate. | |
| * | |
| * **Details** | |
| * | |
| * Returns `true` only when a successful cached value matches and is removed. It | |
| * returns `false` for absent, expired, failed, or non-matching entries. | |
| * | |
| * **Gotchas** | |
| * | |
| * A matching invalidation closes the entry scope and releases its resources. | |
| * | |
| * @see {@link invalidate} for unconditional removal by key | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const invalidateWhen: { | |
| /** | |
| * Invalidates the entry associated with the specified key in the cache when the | |
| * predicate returns true for the cached value. | |
| * | |
| * **When to use** | |
| * | |
| * Use to remove an already-cached scoped value only when the successful cached | |
| * value satisfies a predicate. | |
| * | |
| * **Details** | |
| * | |
| * Returns `true` only when a successful cached value matches and is removed. It | |
| * returns `false` for absent, expired, failed, or non-matching entries. | |
| * | |
| * **Gotchas** | |
| * | |
| * A matching invalidation closes the entry scope and releases its resources. | |
| * | |
| * @see {@link invalidate} for unconditional removal by key | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A>(key: Key, f: Predicate.Predicate<A>): <E, R>(self: ScopedCache<Key, A, E, R>) => Effect.Effect<boolean> | |
| /** | |
| * Invalidates the entry associated with the specified key in the cache when the | |
| * predicate returns true for the cached value. | |
| * | |
| * **When to use** | |
| * | |
| * Use to remove an already-cached scoped value only when the successful cached | |
| * value satisfies a predicate. | |
| * | |
| * **Details** | |
| * | |
| * Returns `true` only when a successful cached value matches and is removed. It | |
| * returns `false` for absent, expired, failed, or non-matching entries. | |
| * | |
| * **Gotchas** | |
| * | |
| * A matching invalidation closes the entry scope and releases its resources. | |
| * | |
| * @see {@link invalidate} for unconditional removal by key | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key, f: Predicate.Predicate<A>): Effect.Effect<boolean> | |
| } = dual( | |
| 3, | |
| <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key, f: Predicate.Predicate<A>): Effect.Effect<boolean> => | |
| effect.uninterruptibleMask((restore) => | |
| core.withFiber((fiber) => | |
| effect.flatMap(getImpl(self, key, fiber, false), (entry) => { | |
| if (entry === undefined) { | |
| return effect.succeed(false) | |
| } | |
| return restore(Deferred.await(entry.deferred)).pipe( | |
| effect.flatMap((value) => { | |
| if (self.state._tag === "Closed") { | |
| return effect.succeed(false) | |
| } else if (f(value)) { | |
| MutableHashMap.remove(self.state.map, key) | |
| return effect.as(Scope.close(entry.scope, effect.exitVoid), true) | |
| } | |
| return effect.succeed(false) | |
| }), | |
| effect.catch_(() => effect.succeed(false)) | |
| ) | |
| }) | |
| ) | |
| ) | |
| ) | |
| /** | |
| * Forces a refresh of the value associated with the specified key in the cache. | |
| * | |
| * **When to use** | |
| * | |
| * Use to recompute a scoped cache entry immediately, even when an unexpired | |
| * value is already cached. | |
| * | |
| * **Details** | |
| * | |
| * It will always invoke the lookup function to construct a new value, | |
| * overwriting any existing value for that key. | |
| * | |
| * @see {@link get} for reusing an unexpired entry before running the lookup | |
| * @see {@link invalidate} for removing an entry without recomputing it | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const refresh: { | |
| /** | |
| * Forces a refresh of the value associated with the specified key in the cache. | |
| * | |
| * **When to use** | |
| * | |
| * Use to recompute a scoped cache entry immediately, even when an unexpired | |
| * value is already cached. | |
| * | |
| * **Details** | |
| * | |
| * It will always invoke the lookup function to construct a new value, | |
| * overwriting any existing value for that key. | |
| * | |
| * @see {@link get} for reusing an unexpired entry before running the lookup | |
| * @see {@link invalidate} for removing an entry without recomputing it | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A>(key: Key): <E, R>(self: ScopedCache<Key, A, E, R>) => Effect.Effect<A, E, R> | |
| /** | |
| * Forces a refresh of the value associated with the specified key in the cache. | |
| * | |
| * **When to use** | |
| * | |
| * Use to recompute a scoped cache entry immediately, even when an unexpired | |
| * value is already cached. | |
| * | |
| * **Details** | |
| * | |
| * It will always invoke the lookup function to construct a new value, | |
| * overwriting any existing value for that key. | |
| * | |
| * @see {@link get} for reusing an unexpired entry before running the lookup | |
| * @see {@link invalidate} for removing an entry without recomputing it | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key): Effect.Effect<A, E, R> | |
| } = dual( | |
| 2, | |
| <Key, A, E, R>(self: ScopedCache<Key, A, E, R>, key: Key): Effect.Effect<A, E, R> => | |
| effect.uninterruptibleMask(effect.fnUntraced(function*(restore) { | |
| if (self.state._tag === "Closed") return yield* effect.interrupt | |
| const fiber = Fiber.getCurrent()! | |
| const scope = Scope.makeUnsafe() | |
| const deferred = Deferred.makeUnsafe</** | |
| * Forces a refresh of the value associated with the specified key in the cache. | |
| * | |
| * **When to use** | |
| * | |
| * Use to recompute a scoped cache entry immediately, even when an unexpired | |
| * value is already cached. | |
| * | |
| * **Details** | |
| * | |
| * It will always invoke the lookup function to construct a new value, | |
| * overwriting any existing value for that key. | |
| * | |
| * @see {@link get} for reusing an unexpired entry before running the lookup | |
| * @see {@link invalidate} for removing an entry without recomputing it | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| A, /** | |
| * Forces a refresh of the value associated with the specified key in the cache. | |
| * | |
| * **When to use** | |
| * | |
| * Use to recompute a scoped cache entry immediately, even when an unexpired | |
| * value is already cached. | |
| * | |
| * **Details** | |
| * | |
| * It will always invoke the lookup function to construct a new value, | |
| * overwriting any existing value for that key. | |
| * | |
| * @see {@link get} for reusing an unexpired entry before running the lookup | |
| * @see {@link invalidate} for removing an entry without recomputing it | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| E>() | |
| const entry: Entry<A, E> = { | |
| scope, | |
| expiresAt: undefined, | |
| deferred | |
| } | |
| const newEntry = !MutableHashMap.has(self.state.map, key) | |
| if (newEntry) { | |
| MutableHashMap.set(self.state.map, key, entry) | |
| yield* checkCapacity(fiber, self.state.map, self.capacity) | |
| } | |
| const exit = yield* effect.exit(restore(Scope.provide(self.lookup(key), scope))) | |
| Deferred.doneUnsafe(deferred, exit) | |
| // @ts-ignore async gap | |
| if (self.state._tag === "Closed") { | |
| if (!newEntry) { | |
| yield* Scope.close(scope, effect.exitVoid) | |
| } | |
| return yield* effect.interrupt | |
| } | |
| const ttl = self.timeToLive(exit, key) | |
| entry.expiresAt = Duration.isFinite(ttl) | |
| ? fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() + Duration.toMillis(ttl) | |
| : undefined | |
| if (!newEntry) { | |
| const oentry = MutableHashMap.get(self.state.map, key) | |
| MutableHashMap.set(self.state.map, key, entry) | |
| if (Option.isSome(oentry)) { | |
| yield* Scope.close(oentry.value.scope, effect.exitVoid) | |
| } | |
| } | |
| return yield* exit | |
| })) | |
| ) | |
| /** | |
| * Removes every entry from the cache and closes each entry scope. | |
| * | |
| * **When to use** | |
| * | |
| * Use to clear a scoped cache and release resources owned by all cached entries. | |
| * | |
| * **Details** | |
| * | |
| * If the cache is closed, the effect is interrupted. | |
| * | |
| * @see {@link invalidate} for removing one cached entry | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const invalidateAll = <Key, A, E, R>(self: ScopedCache<Key, A, E, R>): Effect.Effect<void> => | |
| core.withFiber((parent) => { | |
| if (self.state._tag === "Closed") { | |
| return effect.interrupt | |
| } | |
| return invalidateAllImpl(parent, self.state.map) | |
| }) | |
| const invalidateAllImpl = <Key, A, E>( | |
| parent: Fiber.Fiber<unknown, unknown>, | |
| map: MutableHashMap.MutableHashMap<Key, Entry<A, E>> | |
| ): Effect.Effect<void> => { | |
| const fibers = Arr.empty<Fiber.Fiber<unknown, unknown>>() | |
| for (const [, entry] of map) { | |
| fibers.push(effect.forkUnsafe(parent as any, Scope.close(entry.scope, effect.exitVoid), true, true)) | |
| } | |
| MutableHashMap.clear(map) | |
| return effect.fiberAwaitAll(fibers) | |
| } | |
| /** | |
| * Retrieves the approximate number of entries in the cache. | |
| * | |
| * **When to use** | |
| * | |
| * Use to inspect how many entries are currently stored in the scoped cache. | |
| * | |
| * **Gotchas** | |
| * | |
| * Note that expired entries are counted until they are accessed and removed. | |
| * The size reflects the current number of entries stored, not the number | |
| * of valid entries. | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const size = <Key, A, E, R>(self: ScopedCache<Key, A, E, R>): Effect.Effect<number> => | |
| effect.sync(() => self.state._tag === "Closed" ? 0 : MutableHashMap.size(self.state.map)) | |
| /** | |
| * Retrieves all active keys from the cache, automatically filtering out expired entries. | |
| * | |
| * **When to use** | |
| * | |
| * Use to inspect currently cached unexpired keys without running cache lookups. | |
| * | |
| * **Gotchas** | |
| * | |
| * Expired entries are removed and their scopes are closed while filtering. | |
| * | |
| * @see {@link entries} for retrieving successful cached key-value pairs | |
| * @see {@link values} for retrieving only successfully cached values | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const keys = <Key, A, E, R>(self: ScopedCache<Key, A, E, R>): Effect.Effect<Array<Key>> => | |
| core.withFiber((fiber) => { | |
| if (self.state._tag === "Closed") return effect.succeed([]) | |
| const state = self.state | |
| const now = fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() | |
| const fibers = Arr.empty<Fiber.Fiber<unknown, unknown>>() | |
| const keys: Array<Key> = [] | |
| for (const [key, entry] of state.map) { | |
| if (entry.expiresAt === undefined || entry.expiresAt > now) { | |
| keys.push(key) | |
| } else { | |
| MutableHashMap.remove(state.map, key) | |
| fibers.push(effect.forkUnsafe(fiber, Scope.close(entry.scope, effect.exitVoid), true, true)) | |
| } | |
| } | |
| return fibers.length === 0 ? effect.succeed(keys) : effect.as(effect.fiberAwaitAll(fibers), keys) | |
| }) | |
| /** | |
| * Retrieves all successfully cached values from the cache, excluding failed | |
| * lookups and expired entries. | |
| * | |
| * **When to use** | |
| * | |
| * Use to inspect currently successful cached values without running cache | |
| * lookups. | |
| * | |
| * **Gotchas** | |
| * | |
| * Expired entries are removed and their scopes are closed while filtering. | |
| * | |
| * @see {@link entries} for retrieving successful cached key-value pairs | |
| * @see {@link keys} for retrieving only cached keys | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const values = <Key, A, E, R>(self: ScopedCache<Key, A, E, R>): Effect.Effect<Array<A>> => | |
| effect.map(entries(self), Arr.map(([, value]) => value)) | |
| /** | |
| * Retrieves all key-value pairs from the cache as an array. This function | |
| * only returns entries with successfully resolved values, filtering out any | |
| * failed lookups or expired entries. | |
| * | |
| * **When to use** | |
| * | |
| * Use to inspect the currently successful cached key-value pairs without | |
| * running cache lookups. | |
| * | |
| * **Gotchas** | |
| * | |
| * Expired entries are removed and their scopes are closed while filtering. | |
| * | |
| * @see {@link keys} for retrieving only cached keys | |
| * @see {@link values} for retrieving only cached values | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const entries = <Key, A, E, R>(self: ScopedCache<Key, A, E, R>): Effect.Effect<Array<[Key, A]>> => | |
| core.withFiber((fiber) => { | |
| if (self.state._tag === "Closed") return effect.succeed([]) | |
| const state = self.state | |
| const now = fiber.getRef(effect.ClockRef).currentTimeMillisUnsafe() | |
| const fibers = Arr.empty<Fiber.Fiber<unknown, unknown>>() | |
| const arr: Array<[Key, A]> = [] | |
| for (const [key, entry] of state.map) { | |
| if (entry.expiresAt === undefined || entry.expiresAt > now) { | |
| const exit = entry.deferred.effect | |
| if (core.isExit(exit) && !effect.exitIsFailure(exit)) { | |
| arr.push([key, exit.value as A]) | |
| } | |
| } else { | |
| MutableHashMap.remove(state.map, key) | |
| fibers.push(effect.forkUnsafe(fiber, Scope.close(entry.scope, effect.exitVoid), true, true)) | |
| } | |
| } | |
| return fibers.length === 0 | |
| ? effect.succeed(arr) | |
| : effect.as(effect.fiberAwaitAll(fibers), arr) | |
| }) | |
Xet Storage Details
- Size:
- 43.7 kB
- Xet hash:
- b987141ba4c93db2f9883a78bedc947996b3869b0a5284e9d7bf5c193dd3c05b
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.