From 0078e0bf8f28104234e3df5a0a522729e6aa830a Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 17 Jan 2025 16:08:26 +1300 Subject: [PATCH] add RcMap.touch, for reseting the idle timeout for an item (#4281) --- .changeset/great-buses-perform.md | 5 ++++ packages/effect/src/RcMap.ts | 9 +++++++ packages/effect/src/internal/rcMap.ts | 37 +++++++++++++++++++++++++-- packages/effect/test/RcMap.test.ts | 31 ++++++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 .changeset/great-buses-perform.md diff --git a/.changeset/great-buses-perform.md b/.changeset/great-buses-perform.md new file mode 100644 index 00000000000..875a76566f4 --- /dev/null +++ b/.changeset/great-buses-perform.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +add RcMap.touch, for reseting the idle timeout for an item diff --git a/packages/effect/src/RcMap.ts b/packages/effect/src/RcMap.ts index df99d901517..71061963918 100644 --- a/packages/effect/src/RcMap.ts +++ b/packages/effect/src/RcMap.ts @@ -118,3 +118,12 @@ export const invalidate: { (key: K): (self: RcMap) => Effect.Effect (self: RcMap, key: K): Effect.Effect } = internal.invalidate + +/** + * @since 3.13.0 + * @category combinators + */ +export const touch: { + (key: K): (self: RcMap) => Effect.Effect + (self: RcMap, key: K): Effect.Effect +} = internal.touch diff --git a/packages/effect/src/internal/rcMap.ts b/packages/effect/src/internal/rcMap.ts index 34de2fbe14b..aa93d18712f 100644 --- a/packages/effect/src/internal/rcMap.ts +++ b/packages/effect/src/internal/rcMap.ts @@ -1,4 +1,5 @@ import type * as Cause from "../Cause.js" +import type { Clock } from "../Clock.js" import * as Context from "../Context.js" import type * as Deferred from "../Deferred.js" import * as Duration from "../Duration.js" @@ -34,6 +35,7 @@ declare namespace State { readonly scope: Scope.CloseableScope readonly finalizer: Effect fiber: RuntimeFiber | undefined + expiresAt: number refCount: number } } @@ -168,6 +170,7 @@ const acquire = core.fnUntraced(function*(self: RcMapImpl, key scope, finalizer: undefined as any, fiber: undefined, + expiresAt: 0, refCount: 1 } ;(entry as any).finalizer = release(self, key, entry) @@ -178,7 +181,7 @@ const acquire = core.fnUntraced(function*(self: RcMapImpl, key }) const release = (self: RcMapImpl, key: K, entry: State.Entry) => - core.suspend(() => { + coreEffect.clockWith((clock) => { entry.refCount-- if (entry.refCount > 0) { return core.void @@ -193,7 +196,10 @@ const release = (self: RcMapImpl, key: K, entry: State.Entry { if (self.state._tag === "Open" && entry.refCount === 0) { @@ -213,6 +219,16 @@ const release = (self: RcMapImpl, key: K, entry: State.Entry(entry: State.Entry, clock: Clock) => + core.suspend(function loop(): Effect { + const now = clock.unsafeCurrentTimeMillis() + const remaining = entry.expiresAt - now + if (remaining <= 0) { + return core.void + } + return core.flatMap(clock.sleep(Duration.millis(remaining)), loop) + }) + /** @internal */ export const keys = (self: RcMap.RcMap): Effect> => { const impl = self as RcMapImpl @@ -239,3 +255,20 @@ export const invalidate: { if (entry.fiber) yield* core.interruptFiber(entry.fiber) }) ) + +/** @internal */ +export const touch: { + (key: K): (self: RcMap.RcMap) => Effect + (self: RcMap.RcMap, key: K): Effect +} = dual( + 2, + (self_: RcMap.RcMap, key: K) => + coreEffect.clockWith((clock) => { + const self = self_ as RcMapImpl + if (!self.idleTimeToLive || self.state._tag === "Closed") return core.void + const o = MutableHashMap.get(self.state.map, key) + if (o._tag === "None") return core.void + o.value.expiresAt = clock.unsafeCurrentTimeMillis() + Duration.toMillis(self.idleTimeToLive) + return core.void + }) +) diff --git a/packages/effect/test/RcMap.test.ts b/packages/effect/test/RcMap.test.ts index b45074a155c..d5b24917bb5 100644 --- a/packages/effect/test/RcMap.test.ts +++ b/packages/effect/test/RcMap.test.ts @@ -97,6 +97,37 @@ describe("RcMap", () => { deepStrictEqual(released, ["foo", "bar", "baz"]) })) + it.scoped(".touch", () => + Effect.gen(function*() { + const acquired: Array = [] + const released: Array = [] + const map = yield* RcMap.make({ + lookup: (key: string) => + Effect.acquireRelease( + Effect.sync(() => { + acquired.push(key) + return key + }), + () => Effect.sync(() => released.push(key)) + ), + idleTimeToLive: 1000 + }) + + assert.deepStrictEqual(acquired, []) + assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") + assert.deepStrictEqual(acquired, ["foo"]) + assert.deepStrictEqual(released, []) + + yield* TestClock.adjust(500) + assert.deepStrictEqual(released, []) + + yield* RcMap.touch(map, "foo") + yield* TestClock.adjust(500) + assert.deepStrictEqual(released, []) + yield* TestClock.adjust(500) + assert.deepStrictEqual(released, ["foo"]) + })) + it.scoped("capacity", () => Effect.gen(function*() { const map = yield* RcMap.make({