import * as Context from "../Context.ts" import * as Duration from "../Duration.ts" import * as Effect from "../Effect.ts" import * as Exit from "../Exit.ts" import * as Fiber from "../Fiber.ts" import { identity } from "../Function.ts" import { pipeArguments } from "../Pipeable.ts" import type * as RcRef from "../RcRef.ts" import * as Scope from "../Scope.ts" const TypeId = "~effect/RcRef" type State = State.Empty | State.Acquiring | State.Acquired | State.Closed declare namespace State { interface Empty { readonly _tag: "Empty" } interface Acquiring { readonly _tag: "Acquiring" // Assigned as soon as the acquisition is forked. fiber: Fiber.Fiber, unknown> awaiters: number // Set once references are allocated to the waiters, which can happen // before the acquisition fiber exits. acquired: Acquired | undefined } interface Acquired { readonly _tag: "Acquired" readonly value: A readonly scope: Scope.Closeable fiber: Fiber.Fiber | undefined refCount: number invalidated: boolean } interface Closed { readonly _tag: "Closed" } } const stateEmpty: State = { _tag: "Empty" } const stateClosed: State = { _tag: "Closed" } const variance: RcRef.RcRef.Variance = { _A: identity, _E: identity } class RcRefImpl implements RcRef.RcRef { readonly [TypeId]: RcRef.RcRef.Variance = variance pipe() { return pipeArguments(this, arguments) } state: State = stateEmpty readonly acquire: Effect.Effect readonly context: Context.Context readonly scope: Scope.Scope readonly idleTimeToLive: Duration.Duration | undefined constructor( acquire: Effect.Effect, context: Context.Context, scope: Scope.Scope, idleTimeToLive: Duration.Duration | undefined ) { this.acquire = acquire this.context = context this.scope = scope this.idleTimeToLive = idleTimeToLive } } /** @internal */ export const make = (options: { readonly acquire: Effect.Effect readonly idleTimeToLive?: Duration.Input | undefined }) => Effect.withFiber, never, R | Scope.Scope>((fiber) => { const context = fiber.context as Context.Context const scope = Context.get(context, Scope.Scope) const ref = new RcRefImpl( options.acquire as Effect.Effect, context, scope, options.idleTimeToLive !== undefined ? Duration.fromInputUnsafe(options.idleTimeToLive) : undefined ) return Effect.as( Scope.addFinalizerExit(scope, () => { const state = ref.state ref.state = stateClosed if (state._tag === "Acquired") return Scope.close(state.scope, Exit.void) if (state._tag === "Acquiring") return Fiber.interrupt(state.fiber) return Effect.void }), ref ) }) const getState = ( self: RcRefImpl, restore: (effect: Effect.Effect) => Effect.Effect ): Effect.Effect, E> => { switch (self.state._tag) { case "Closed": { return Effect.interrupt } case "Acquired": { const state = self.state state.refCount++ return state.fiber ? Effect.as(Fiber.interrupt(state.fiber), state) : Effect.succeed(state) } case "Acquiring": { const state = self.state state.awaiters++ return awaitAcquiring(self, state, restore) } case "Empty": { // The caller that starts the acquisition counts as its first awaiter. const acquiring: State.Acquiring = { _tag: "Acquiring", fiber: undefined as any, awaiters: 1, acquired: undefined } self.state = acquiring const scope = Scope.makeUnsafe() // Acquire on a detached fiber so the acquisition is shared by every // caller and only interrupted once all of them have been interrupted. const acquire = Effect.provideContext( self.acquire as Effect.Effect, Context.add(self.context, Scope.Scope, scope) ).pipe( Effect.flatMap((value) => { // The ref was closed, or the acquisition was abandoned. if (self.state !== acquiring) { return Effect.interrupt } const state: State.Acquired = { _tag: "Acquired", value, scope, fiber: undefined, // Every caller still waiting holds a reference. refCount: acquiring.awaiters, invalidated: false } self.state = state acquiring.acquired = state return Effect.succeed(state) }), Effect.onExit((exit) => { if (Exit.isSuccess(exit)) return Effect.void if (self.state === acquiring) self.state = stateEmpty return Scope.close(scope, exit) }) ) acquiring.fiber = Effect.runForkWith(Fiber.getCurrent()!.context)(acquire) return awaitAcquiring(self, acquiring, restore) } } } const awaitAcquiring = ( self: RcRefImpl, acquiring: State.Acquiring, restore: (effect: Effect.Effect) => Effect.Effect ): Effect.Effect, E> => Effect.onExit( restore(Fiber.join(acquiring.fiber)) as Effect.Effect, E>, (exit) => { const acquired = acquiring.acquired if (acquired !== undefined) { // The acquisition counted this caller as a reference, possibly before // its fiber exited. Release it if the caller was interrupted before it // could register its finalizer. return Exit.isFailure(exit) ? release(self, acquired) : Effect.void } // Abandon the acquisition once every caller has left, unless it already // failed (a failed acquisition allocated no references). if (--acquiring.awaiters > 0 || self.state !== acquiring) return Effect.void // Detach it first so new callers start a fresh acquisition instead of // joining one that is being interrupted. self.state = stateEmpty return Fiber.interrupt(acquiring.fiber) } ) /** @internal */ export const get = (self_: RcRef.RcRef): Effect.Effect => { const self = self_ as RcRefImpl return Effect.uninterruptibleMask((restore) => Effect.flatMap( Effect.scope, (scope) => Effect.flatMap(getState(self, restore), (state) => Effect.as(Scope.addFinalizerExit(scope, () => release(self, state)), state.value)) ) ) } const release = (self: RcRefImpl, state: State.Acquired): Effect.Effect => { state.refCount-- if (state.refCount > 0) { return Effect.void } const idleTimeToLive = self.idleTimeToLive if (idleTimeToLive === undefined || state.invalidated) { if (self.state === state) { self.state = stateEmpty } return Scope.close(state.scope, Exit.void) } else if (!Duration.isFinite(idleTimeToLive)) { return Effect.void } state.fiber = Effect.uninterruptibleMask((restore) => Effect.flatMap(restore(Effect.sleep(idleTimeToLive)), () => { if (self.state === state && state.refCount === 0) { self.state = stateEmpty return Scope.close(state.scope, Exit.void) } return Effect.void }) ).pipe( Effect.ensuring(Effect.sync(() => { state.fiber = undefined })), Effect.runForkWith(self.context), Fiber.runIn(self.scope) ) return Effect.void } /** @internal */ export const invalidate = ( self_: RcRef.RcRef ): Effect.Effect => { const self = self_ as RcRefImpl return Effect.uninterruptible(Effect.suspend(() => { if (self.state._tag !== "Acquired") { return Effect.void } const state = self.state self.state = stateEmpty state.invalidated = true if (state.refCount > 0) { return Effect.void } state.fiber?.interruptUnsafe() return Scope.close(state.scope, Exit.void) })) }