1/**
2 * Shares scoped resources by key and releases them when no one is using them.
3 *
4 * An `RcMap` runs a lookup effect the first time a key is requested, shares the
5 * in-progress or acquired resource with other callers for the same key, and
6 * tracks each caller through its current `Scope`. When the last scope for a key
7 * closes, the resource can be released, kept alive for an idle time, or removed
8 * by capacity limits or explicit invalidation. It is meant for resource
9 * lifecycles such as clients, sessions, and connections, not as a general
10 * mutable cache.
11 *
12 * @since 3.5.0
13 */
14import * as Cause from "./Cause.ts"
15import { Clock } from "./Clock.ts"
16import * as Context from "./Context.ts"
17import * as Deferred from "./Deferred.ts"
18import * as Duration from "./Duration.ts"
19import * as Effect from "./Effect.ts"
20import * as Exit from "./Exit.ts"
21import * as Fiber from "./Fiber.ts"
22import { constant, dual, flow } from "./Function.ts"
23import * as MutableHashMap from "./MutableHashMap.ts"
24import type * as Option from "./Option.ts"
25import type { Pipeable } from "./Pipeable.ts"
26import { pipeArguments } from "./Pipeable.ts"
27import * as Scope from "./Scope.ts"
28
29const TypeId = "~effect/RcMap"
30
31/**
32 * An `RcMap` is a reference-counted map data structure that manages the lifecycle
33 * of resources indexed by keys. Resources are lazily acquired and automatically
34 * released when no longer in use.
35 *
36 * **When to use**
37 *
38 * Use to share scoped resources by key while automatically releasing them after
39 * their last active reference is gone.
40 *
41 * **Example** (Inspecting a reference-counted map)
42 *
43 * ```ts import.meta.vitest
44 * import { Effect, RcMap } from "effect"
45 *
46 * const program = Effect.gen(function*() {
47 * // Create an RcMap that manages database connections
48 * const dbConnectionMap = yield* RcMap.make({
49 * lookup: (dbName: string) =>
50 * Effect.acquireRelease(Effect.succeed(`Connection to ${dbName}`), () => Effect.void),
51 * capacity: 10,
52 * idleTimeToLive: "5 minutes"
53 * })
54 *
55 * // The RcMap interface provides access to:
56 * // - lookup: Function to acquire resources
57 * // - capacity: Maximum number of resources
58 * // - idleTimeToLive: Time before idle resources are released
59 * // - state: Current state of the map
60 *
61 * return dbConnectionMap.capacity
62 * })
63 *
64 * await Effect.runPromise(Effect.scoped(program)) // => 10
65 * ```
66 *
67 * @see {@link make} for creating an `RcMap`
68 * @see {@link get} for acquiring or retaining a resource by key
69 *
70 * @category models
71 * @since 3.5.0
72 */
73export interface RcMap<in out K, in out A, in out E = never> extends Pipeable {
74 readonly [TypeId]: typeof TypeId
75 readonly lookup: (key: K) => Effect.Effect<A, E, Scope.Scope>
76 readonly context: Context.Context<never>
77 readonly scope: Scope.Scope
78 readonly idleTimeToLive: (key: K) => Duration.Duration
79 readonly capacity: number
80 state: State<K, A, E>
81}
82
83/**
84 * Represents the internal state of an RcMap, which can be either Open (active)
85 * or Closed (shutdown and no longer accepting operations).
86 *
87 * **When to use**
88 *
89 * Use when typing code that inspects an `RcMap`'s `state` field and narrows
90 * between open and closed lifecycle states.
91 *
92 * @see {@link RcMap} for the map value that exposes this state
93 * @see {@link State.Open} for the active state with entries
94 * @see {@link State.Closed} for the shutdown state
95 *
96 * @category models
97 * @since 4.0.0
98 */
99export type State<K, A, E> = State.Open<K, A, E> | State.Closed
100
101/**
102 * Namespace containing the internal state types for RcMap.
103 *
104 * **When to use**
105 *
106 * Use when referring to the concrete open, closed, and entry state shapes used
107 * by `RcMap`.
108 *
109 * @since 4.0.0
110 */
111export declare namespace State {
112 /**
113 * Represents the open/active state of an RcMap, containing the actual
114 * resource map that stores entries.
115 *
116 * **When to use**
117 *
118 * Use when handling an `RcMap` that can still accept operations and contains
119 * stored entries.
120 *
121 * @category models
122 * @since 4.0.0
123 */
124 export interface Open<K, A, E> {
125 readonly _tag: "Open"
126 readonly map: MutableHashMap.MutableHashMap<K, Entry<A, E>>
127 }
128
129 /**
130 * Represents the closed state of an RcMap, indicating that the map has been
131 * shut down and will no longer accept new operations.
132 *
133 * **When to use**
134 *
135 * Use when handling an `RcMap` after its owning scope has closed.
136 *
137 * @category models
138 * @since 4.0.0
139 */
140 export interface Closed {
141 readonly _tag: "Closed"
142 }
143
144 /**
145 * Represents an individual entry in the RcMap, containing the resource's
146 * metadata including reference count, expiration time, and lifecycle management.
147 *
148 * **When to use**
149 *
150 * Use when inspecting the stored resource, reference count, and idle lifecycle
151 * metadata for a single key.
152 *
153 * @category models
154 * @since 4.0.0
155 */
156 export interface Entry<A, E> {
157 readonly deferred: Deferred.Deferred<A, E>
158 readonly scope: Scope.Closeable
159 readonly finalizer: Effect.Effect<void>
160 readonly idleTimeToLive: Duration.Duration
161 fiber: Fiber.Fiber<void> | undefined
162 expiresAt: number
163 refCount: number
164 }
165}
166
167const makeUnsafe = <K, A, E>(options: {
168 readonly lookup: (key: K) => Effect.Effect<A, E, Scope.Scope>
169 readonly context: Context.Context<never>
170 readonly scope: Scope.Scope
171 readonly idleTimeToLive: (key: K) => Duration.Duration
172 readonly capacity: number
173}): RcMap<K, A, E> => ({
174 [TypeId]: TypeId,
175 lookup: options.lookup,
176 context: options.context,
177 scope: options.scope,
178 idleTimeToLive: options.idleTimeToLive,
179 capacity: options.capacity,
180 state: {
181 _tag: "Open",
182 map: MutableHashMap.empty()
183 },
184 pipe() {
185 return pipeArguments(this, arguments)
186 }
187})
188
189/**
190 * Creates an `RcMap` that can contain multiple reference counted resources that can be indexed
191 * by a key. The resources are lazily acquired on the first call to `get` and
192 * released when the last reference is released.
193 *
194 * **When to use**
195 *
196 * Use to create a scoped reference-counted map for resources that should be
197 * acquired once per key and shared while in use.
198 *
199 * **Details**
200 *
201 * Complex keys can extend `Equal` and `Hash` to allow lookups by value.
202 *
203 * - `capacity`: The maximum number of resources that can be held in the map.
204 * - `idleTimeToLive`: When the reference count reaches zero, the resource will be released after this duration.
205 *
206 * **Example** (Creating a reference-counted map)
207 *
208 * ```ts import.meta.vitest
209 * import { Effect, RcMap } from "effect"
210 *
211 * const events: Array<string> = []
212 *
213 * const program = Effect.gen(function*() {
214 * const map = yield* RcMap.make({
215 * lookup: (key: string) =>
216 * Effect.acquireRelease(
217 * Effect.succeed(`acquired ${key}`),
218 * () => Effect.sync(() => events.push(`released ${key}`))
219 * )
220 * })
221 *
222 * // Get "foo" from the map twice, which will only acquire it once.
223 * // It will then be released once the scope closes.
224 * yield* RcMap.get(map, "foo").pipe(
225 * Effect.andThen(RcMap.get(map, "foo")),
226 * Effect.scoped
227 * )
228 * })
229 *
230 * await Effect.runPromise(Effect.scoped(program))
231 * events // => ["released foo"]
232 * ```
233 *
234 * @see {@link get} for acquiring or retaining a resource by key
235 * @see {@link invalidate} for removing a resource from the map
236 *
237 * @category constructors
238 * @since 3.5.0
239 */
240export const make: {
241 <K, A, E, R>(options: {
242 readonly lookup: (key: K) => Effect.Effect<A, E, R>
243 readonly idleTimeToLive?: Duration.Input | ((key: K) => Duration.Input) | undefined
244 readonly capacity?: undefined
245 }): Effect.Effect<RcMap<K, A, E>, never, Scope.Scope | R>
246 <K, A, E, R>(options: {
247 readonly lookup: (key: K) => Effect.Effect<A, E, R>
248 readonly idleTimeToLive?: Duration.Input | ((key: K) => Duration.Input) | undefined
249 readonly capacity: number
250 }): Effect.Effect<RcMap<K, A, E | Cause.ExceededCapacityError>, never, Scope.Scope | R>
251} = <K, A, E, R>(options: {
252 readonly lookup: (key: K) => Effect.Effect<A, E, R>
253 readonly idleTimeToLive?: Duration.Input | ((key: K) => Duration.Input) | undefined
254 readonly capacity?: number | undefined
255}) =>
256 Effect.withFiber<RcMap<K, A, E>, never, R | Scope.Scope>((fiber) => {
257 const context = fiber.context as Context.Context<R | Scope.Scope>
258 const scope = Context.get(context, Scope.Scope)
259 const self = makeUnsafe<K, A, E>({
260 lookup: options.lookup as any,
261 context,
262 scope,
263 idleTimeToLive: typeof options.idleTimeToLive === "function"
264 ? flow(options.idleTimeToLive, Duration.fromInputUnsafe)
265 : constant(Duration.fromInputUnsafe(options.idleTimeToLive ?? Duration.zero)),
266 capacity: Math.max(options.capacity ?? Number.POSITIVE_INFINITY, 0)
267 })
268 return Effect.as(
269 Scope.addFinalizerExit(scope, () => {
270 if (self.state._tag === "Closed") {
271 return Effect.void
272 }
273 const map = self.state.map
274 self.state = { _tag: "Closed" }
275 return Effect.forEach(
276 map,
277 ([, entry]) => Effect.exit(closeEntry(entry))
278 ).pipe(
279 Effect.tap(() =>
280 Effect.sync(() => {
281 MutableHashMap.clear(map)
282 })
283 )
284 )
285 }),
286 self
287 )
288 })
289
290/**
291 * Gets the resource for a key, acquiring it with the map's lookup function when
292 * the key is not already cached.
293 *
294 * **When to use**
295 *
296 * Use to acquire or retain the resource for a key within the current scope.
297 *
298 * **Details**
299 *
300 * The resource's reference count is incremented for the current `Scope`, and a
301 * release finalizer is added to that scope. When the current scope closes, the
302 * reference is released; the resource is closed when the last reference is
303 * released, subject to the map's idle time-to-live setting.
304 *
305 * **Example** (Acquiring a resource)
306 *
307 * ```ts import.meta.vitest
308 * import { Effect, RcMap } from "effect"
309 *
310 * const events: Array<string> = []
311 *
312 * const program = Effect.gen(function*() {
313 * const map = yield* RcMap.make({
314 * lookup: (key: string) =>
315 * Effect.acquireRelease(
316 * Effect.succeed(`Resource: ${key}`),
317 * () => Effect.sync(() => events.push(`released ${key}`))
318 * )
319 * })
320 *
321 * // Get a resource - it will be acquired on first access
322 * const resource = yield* RcMap.get(map, "database")
323 * return [resource, events] as const
324 * })
325 *
326 * await Effect.runPromise(Effect.scoped(program)) // => ["Resource: database", ["released database"]]
327 * ```
328 *
329 * @see {@link make} for creating the reference-counted map
330 * @see {@link invalidate} for removing a resource by key
331 *
332 * @category combinators
333 * @since 3.5.0
334 */
335export const get: {
336 <K>(key: K): <A, E>(self: RcMap<K, A, E>) => Effect.Effect<A, E, Scope.Scope>
337 <K, A, E>(self: RcMap<K, A, E>, key: K): Effect.Effect<A, E, Scope.Scope>
338} = dual(
339 2,
340 <K, A, E>(self: RcMap<K, A, E>, key: K): Effect.Effect<A, E, Scope.Scope> =>
341 Effect.uninterruptibleMask((restore) => {
342 if (self.state._tag === "Closed") {
343 return Effect.interrupt
344 }
345 const state = self.state
346 const parent = Fiber.getCurrent()!
347 const o = MutableHashMap.get(state.map, key)
348 let entry: State.Entry<A, E>
349 if (o._tag === "Some") {
350 entry = o.value
351 entry.refCount++
352 } else if (Number.isFinite(self.capacity) && MutableHashMap.size(self.state.map) >= self.capacity) {
353 return Effect.fail(
354 new Cause.ExceededCapacityError(`RcMap attempted to exceed capacity of ${self.capacity}`)
355 ) as Effect.Effect<never>
356 } else {
357 entry = {
358 deferred: Deferred.makeUnsafe(),
359 scope: Scope.makeUnsafe(),
360 idleTimeToLive: self.idleTimeToLive(key),
361 finalizer: undefined as any,
362 fiber: undefined,
363 expiresAt: 0,
364 refCount: 1
365 }
366 ;(entry as any).finalizer = release(self, key, entry)
367 MutableHashMap.set(state.map, key, entry)
368 const context = new Map(self.context.mapUnsafe)
369 parent.context.mapUnsafe.forEach((value, key) => {
370 context.set(key, value)
371 })
372 context.set(Scope.Scope.key, entry.scope)
373 const lookupContext: Context.Context<Scope.Scope> = Context.makeUnsafe(context)
374 Effect.suspend(() => self.lookup(key)).pipe(
375 Effect.runForkWith(lookupContext),
376 Fiber.runIn(entry.scope)
377 ).addObserver((exit) => {
378 // Interruption is abandonment, so the entry is dropped and the next
379 // get starts a fresh lookup.
380 if (Exit.hasInterrupts(exit)) removeInterrupted(self, key, entry, lookupContext)
381 Deferred.doneUnsafe(entry.deferred, exit)
382 })
383 }
384 const scope = Context.getUnsafe(parent.context, Scope.Scope)
385 return Scope.addFinalizer(scope, entry.finalizer).pipe(
386 Effect.andThen(restore(Deferred.await(entry.deferred)))
387 )
388 })
389)
390
391/**
392 * Retains and returns an existing resource without invoking the map's lookup
393 * function when the key is missing.
394 *
395 * **When to use**
396 *
397 * Use when you only want to acquire a reference to a resource that is currently
398 * cached.
399 *
400 * **Details**
401 *
402 * Returns `Option.none` when the key is not currently stored or the map is
403 * closed. If an entry exists, its reference count is incremented for the current
404 * `Scope` before awaiting its result. A successful entry returns
405 * `Option.some(value)`, while an in-flight or cached failure fails with the same
406 * error as `get`.
407 *
408 * **Example** (Retaining only cached resources)
409 *
410 * ```ts import.meta.vitest
411 * import { Effect, Option, RcMap } from "effect"
412 *
413 * const program = Effect.gen(function*() {
414 * const map = yield* RcMap.make({
415 * lookup: (key: string) => Effect.succeed(`Resource: ${key}`),
416 * idleTimeToLive: "1 minute"
417 * })
418 *
419 * const missing = yield* RcMap.getOption(map, "database")
420 * yield* Effect.scoped(RcMap.get(map, "database"))
421 * const cached = yield* Effect.scoped(RcMap.getOption("database")(map))
422 *
423 * return [missing, cached] as const
424 * })
425 *
426 * await Effect.runPromise(Effect.scoped(program)) // => [Option.none(), Option.some("Resource: database")]
427 * ```
428 *
429 * @see {@link get} for acquiring a resource when the key is missing
430 * @see {@link has} for checking presence without retaining or awaiting the entry
431 *
432 * @category combinators
433 * @since 4.0.0
434 */
435export const getOption: {
436 <K>(key: K): <A, E>(self: RcMap<K, A, E>) => Effect.Effect<Option.Option<A>, E, Scope.Scope>
437 <K, A, E>(self: RcMap<K, A, E>, key: K): Effect.Effect<Option.Option<A>, E, Scope.Scope>
438} = dual(
439 2,
440 <K, A, E>(self: RcMap<K, A, E>, key: K): Effect.Effect<Option.Option<A>, E, Scope.Scope> =>
441 Effect.uninterruptibleMask((restore) => {
442 if (self.state._tag === "Closed") {
443 return Effect.succeedNone
444 }
445 const o = MutableHashMap.get(self.state.map, key)
446 if (o._tag === "None") {
447 return Effect.succeedNone
448 }
449 const entry = o.value
450 entry.refCount++
451 const scope = Context.getUnsafe(Fiber.getCurrent()!.context, Scope.Scope)
452 return Scope.addFinalizer(scope, entry.finalizer).pipe(
453 Effect.andThen(Effect.asSome(restore(Deferred.await(entry.deferred))))
454 )
455 })
456)
457
458const removeInterrupted = <K, A, E>(
459 self: RcMap<K, A, E>,
460 key: K,
461 entry: State.Entry<A, E>,
462 context: Context.Context<Scope.Scope>
463) => {
464 if (self.state._tag === "Closed") return
465 const o = MutableHashMap.get(self.state.map, key)
466 if (o._tag === "None" || o.value !== entry) return
467 MutableHashMap.remove(self.state.map, key)
468 // Borrowers close the entry on release; an idle entry has none left.
469 if (entry.refCount > 0) return
470 closeEntry(entry).pipe(
471 Effect.runForkWith(context),
472 Fiber.runIn(self.scope)
473 )
474}
475
476const closeEntry = <A, E>(entry: State.Entry<A, E>) =>
477 entry.fiber
478 ? Fiber.interrupt(entry.fiber).pipe(Effect.andThen(Scope.close(entry.scope, Exit.void)))
479 : Scope.close(entry.scope, Exit.void)
480
481const release = <K, A, E>(self: RcMap<K, A, E>, key: K, entry: State.Entry<A, E>) =>
482 Effect.withFiber((fiber) => {
483 entry.refCount--
484 if (entry.refCount > 0) {
485 return Effect.void
486 } else if (self.state._tag === "Closed") {
487 return closeEntry(entry)
488 }
489
490 const o = MutableHashMap.get(self.state.map, key)
491 if (o._tag === "None" || o.value !== entry) {
492 return closeEntry(entry)
493 } else if (Duration.isZero(entry.idleTimeToLive)) {
494 MutableHashMap.remove(self.state.map, key)
495 return closeEntry(entry)
496 } else if (!Duration.isFinite(entry.idleTimeToLive)) {
497 return Effect.void
498 }
499
500 const clock = fiber.getRef(Clock)
501 entry.expiresAt = clock.currentTimeMillisUnsafe() + Duration.toMillis(entry.idleTimeToLive)
502 if (entry.fiber) return Effect.void
503
504 entry.fiber = Effect.uninterruptibleMask(function loop(restore): Effect.Effect<void> {
505 const now = clock.currentTimeMillisUnsafe()
506 const remaining = entry.expiresAt - now
507 if (remaining <= 0) {
508 if (self.state._tag === "Closed" || entry.refCount > 0) return Effect.void
509 const o = MutableHashMap.get(self.state.map, key)
510 if (o._tag === "None" || o.value !== entry) return Effect.void
511 MutableHashMap.remove(self.state.map, key)
512 return Scope.close(entry.scope, Exit.void)
513 }
514 return Effect.flatMap(restore(clock.sleep(Duration.millis(remaining))), () => loop(restore))
515 }).pipe(
516 Effect.ensuring(Effect.sync(() => {
517 entry.fiber = undefined
518 })),
519 Effect.runForkWith(fiber.context),
520 Fiber.runIn(self.scope)
521 )
522 return Effect.void
523 })
524
525/**
526 * Returns an iterable of all keys currently stored in the `RcMap`.
527 *
528 * **When to use**
529 *
530 * Use to inspect which keys currently have stored resources in an `RcMap`.
531 *
532 * **Details**
533 *
534 * If the `RcMap` has been closed, the effect is interrupted.
535 *
536 * **Example** (Listing keys)
537 *
538 * ```ts import.meta.vitest
539 * import { Effect, RcMap } from "effect"
540 *
541 * const program = Effect.gen(function*() {
542 * const map = yield* RcMap.make({
543 * lookup: (key: string) => Effect.succeed(`value-${key}`)
544 * })
545 *
546 * // Add some resources to the map
547 * yield* RcMap.get(map, "foo")
548 * yield* RcMap.get(map, "bar")
549 * yield* RcMap.get(map, "baz")
550 *
551 * // Get all keys currently in the map
552 * const allKeys = yield* RcMap.keys(map)
553 * return Array.from(allKeys)
554 * })
555 *
556 * await Effect.runPromise(Effect.scoped(program)) // => ["foo", "bar", "baz"]
557 * ```
558 *
559 * @see {@link has} for checking one key without enumerating all keys
560 *
561 * @category combinators
562 * @since 3.8.0
563 */
564export const keys = <K, A, E>(self: RcMap<K, A, E>): Effect.Effect<Iterable<K>> => {
565 return Effect.suspend(() =>
566 self.state._tag === "Closed" ? Effect.interrupt : Effect.succeed(MutableHashMap.keys(self.state.map))
567 )
568}
569
570/**
571 * Invalidates and removes a specific key from the RcMap. If the resource is not
572 * currently in use (reference count is 0), it will be immediately released.
573 *
574 * **When to use**
575 *
576 * Use to remove a resource by key so the next access performs a fresh lookup.
577 *
578 * **Example** (Invalidating a resource)
579 *
580 * ```ts import.meta.vitest
581 * import { Effect, RcMap } from "effect"
582 *
583 * const events: Array<string> = []
584 *
585 * const program = Effect.gen(function*() {
586 * const map = yield* RcMap.make({
587 * lookup: (key: string) =>
588 * Effect.acquireRelease(
589 * Effect.succeed(`Resource: ${key}`),
590 * () => Effect.sync(() => events.push(`released ${key}`))
591 * )
592 * })
593 *
594 * // Get a resource
595 * yield* RcMap.get(map, "cache")
596 *
597 * // Invalidate the resource - it will be removed from the map
598 * // and released if no longer in use
599 * yield* RcMap.invalidate(map, "cache")
600 *
601 * // Next access will create a new resource
602 * yield* RcMap.get(map, "cache")
603 * })
604 *
605 * await Effect.runPromise(Effect.scoped(program))
606 * events // => ["released cache", "released cache"]
607 * ```
608 *
609 * @see {@link get} for acquiring or retaining the resource for a key
610 * @see {@link touch} for extending the idle lifetime without removing the entry
611 *
612 * @category combinators
613 * @since 3.13.0
614 */
615export const invalidate: {
616 <K>(key: K): <A, E>(self: RcMap<K, A, E>) => Effect.Effect<void>
617 <K, A, E>(self: RcMap<K, A, E>, key: K): Effect.Effect<void>
618} = dual(
619 2,
620 Effect.fnUntraced(function*<K, A, E>(self: RcMap<K, A, E>, key: K) {
621 if (self.state._tag === "Closed") return
622 const o = MutableHashMap.get(self.state.map, key)
623 if (o._tag === "None") return
624 const entry = o.value
625 MutableHashMap.remove(self.state.map, key)
626 if (entry.refCount > 0) return
627 yield* closeEntry(entry)
628 }, Effect.uninterruptible)
629)
630
631/**
632 * Returns whether the `RcMap` currently contains an entry for the specified
633 * key.
634 *
635 * **When to use**
636 *
637 * Use to check whether a key is already present in an `RcMap` without running
638 * the lookup function or acquiring a missing resource.
639 *
640 * **Details**
641 *
642 * This operation only checks the current map state.
643 *
644 * **Gotchas**
645 *
646 * Closed maps return `false`, so `false` does not distinguish a missing key
647 * from a closed map.
648 *
649 * @see {@link get} for acquiring or retaining the resource for a key
650 * @see {@link keys} for enumerating all currently stored keys
651 *
652 * @category combinators
653 * @since 3.17.7
654 */
655export const has: {
656 <K>(key: K): <A, E>(self: RcMap<K, A, E>) => Effect.Effect<boolean>
657 <K, A, E>(self: RcMap<K, A, E>, key: K): Effect.Effect<boolean>
658} = dual(
659 2,
660 <K, A, E>(self: RcMap<K, A, E>, key: K) =>
661 Effect.sync(() => {
662 if (self.state._tag === "Closed") return false
663 return MutableHashMap.has(self.state.map, key)
664 })
665)
666
667/**
668 * Extends the idle time for a resource in the RcMap. If the RcMap has an
669 * `idleTimeToLive` configured, calling `touch` will reset the expiration
670 * timer for the specified key.
671 *
672 * **When to use**
673 *
674 * Use to keep an idle resource alive longer without acquiring a new reference.
675 *
676 * **Example** (Extending resource idle time)
677 *
678 * ```ts import.meta.vitest
679 * import { Effect, RcMap } from "effect"
680 *
681 * const events: Array<string> = []
682 *
683 * const program = Effect.gen(function*() {
684 * const map = yield* RcMap.make({
685 * lookup: (key: string) =>
686 * Effect.acquireRelease(
687 * Effect.succeed(`Resource: ${key}`),
688 * () => Effect.sync(() => events.push(`released ${key}`))
689 * ),
690 * idleTimeToLive: "10 seconds"
691 * })
692 *
693 * // Get a resource
694 * yield* RcMap.get(map, "session")
695 *
696 * // Touch the resource to extend its idle time
697 * // This resets the 10-second expiration timer
698 * yield* RcMap.touch(map, "session")
699 *
700 * // The resource will now live for another 10 seconds
701 * // from the time it was touched
702 * })
703 *
704 * await Effect.runPromise(Effect.scoped(program))
705 * events // => ["released session"]
706 * ```
707 *
708 * @see {@link invalidate} for removing the resource instead of extending it
709 *
710 * @category combinators
711 * @since 3.13.0
712 */
713export const touch: {
714 <K>(key: K): <A, E>(self: RcMap<K, A, E>) => Effect.Effect<void>
715 <K, A, E>(self: RcMap<K, A, E>, key: K): Effect.Effect<void>
716} = dual(
717 2,
718 <K, A, E>(self: RcMap<K, A, E>, key: K) =>
719 Effect.clockWith((clock) => {
720 if (self.state._tag === "Closed") {
721 return Effect.void
722 }
723 const o = MutableHashMap.get(self.state.map, key)
724 if (o._tag === "None" || Duration.isZero(o.value.idleTimeToLive)) {
725 return Effect.void
726 }
727 const entry = o.value
728 entry.expiresAt = clock.currentTimeMillisUnsafe() + Duration.toMillis(entry.idleTimeToLive)
729 return Effect.void
730 })
731)