import * as Context from "../Context.js"
import * as Duration from "../Duration.js"
import type { Effect } from "../Effect.js"
import * as Effectable from "../Effectable.js"
import type { RuntimeFiber } from "../Fiber.js"
import { identity } from "../Function.js"
import type * as RcRef from "../RcRef.js"
import * as Readable from "../Readable.js"
import type * as Scope from "../Scope.js"
import * as coreEffect from "./core-effect.js"
import * as core from "./core.js"
import * as circular from "./effect/circular.js"
import * as fiberRuntime from "./fiberRuntime.js"
/** @internal */
export const TypeId: RcRef.TypeId = Symbol.for("effect/RcRef") as RcRef.TypeId
type State = State.Empty | State.Acquired | State.Closed
declare namespace State {
interface Empty {
readonly _tag: "Empty"
}
interface Acquired {
readonly _tag: "Acquired"
readonly value: A
readonly scope: Scope.CloseableScope
fiber: RuntimeFiber | undefined
refCount: number
}
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 extends Effectable.Class implements RcRef.RcRef {
readonly [TypeId]: RcRef.RcRef.Variance = variance
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
state: State = stateEmpty
readonly semaphore = circular.unsafeMakeSemaphore(1)
constructor(
readonly acquire: Effect,
readonly context: Context.Context,
readonly scope: Scope.Scope,
readonly idleTimeToLive: Duration.Duration | undefined
) {
super()
this.get = get(this)
}
readonly get: Effect
commit() {
return this.get
}
}
/** @internal */
export const make = (options: {
readonly acquire: Effect
readonly idleTimeToLive?: Duration.DurationInput | undefined
}) =>
core.withFiberRuntime, never, R | Scope.Scope>((fiber) => {
const context = fiber.getFiberRef(core.currentContext) as Context.Context
const scope = Context.get(context, fiberRuntime.scopeTag)
const ref = new RcRefImpl(
options.acquire as Effect,
context,
scope,
options.idleTimeToLive ? Duration.decode(options.idleTimeToLive) : undefined
)
return core.as(
scope.addFinalizer(() =>
ref.semaphore.withPermits(1)(core.suspend(() => {
const close = ref.state._tag === "Acquired"
? core.scopeClose(ref.state.scope, core.exitVoid)
: core.void
ref.state = stateClosed
return close
}))
),
ref
)
})
/** @internal */
export const get = (
self_: RcRef.RcRef
): Effect => {
const self = self_ as RcRefImpl
const isInfinite = self.idleTimeToLive && !Duration.isFinite(self.idleTimeToLive)
return core.uninterruptibleMask((restore) =>
core.suspend(() => {
switch (self.state._tag) {
case "Closed": {
return core.interrupt
}
case "Acquired": {
self.state.refCount++
return self.state.fiber
? core.as(core.interruptFiber(self.state.fiber), self.state)
: core.succeed(self.state)
}
case "Empty": {
return fiberRuntime.scopeMake().pipe(
coreEffect.bindTo("scope"),
coreEffect.bind("value", ({ scope }) =>
restore(core.fiberRefLocally(
self.acquire as Effect,
core.currentContext,
Context.add(self.context, fiberRuntime.scopeTag, scope)
))),
core.map(({ scope, value }) => {
const state: State.Acquired = {
_tag: "Acquired",
value,
scope,
fiber: undefined,
refCount: 1
}
self.state = state
return state
})
)
}
}
})
).pipe(
self.semaphore.withPermits(1),
coreEffect.bindTo("state"),
coreEffect.bind("scope", () => fiberRuntime.scopeTag),
core.tap(({ scope, state }) =>
scope.addFinalizer(() =>
core.suspend(() => {
state.refCount--
if (state.refCount > 0 || isInfinite) {
return core.void
}
if (self.idleTimeToLive === undefined) {
self.state = stateEmpty
return core.scopeClose(state.scope, core.exitVoid)
}
return coreEffect.sleep(self.idleTimeToLive).pipe(
core.interruptible,
core.zipRight(core.suspend(() => {
if (self.state._tag === "Acquired" && self.state.refCount === 0) {
self.state = stateEmpty
return core.scopeClose(state.scope, core.exitVoid)
}
return core.void
})),
fiberRuntime.ensuring(core.sync(() => {
state.fiber = undefined
})),
circular.forkIn(self.scope),
core.tap((fiber) => {
state.fiber = fiber
}),
self.semaphore.withPermits(1)
)
})
)
),
core.map(({ state }) => state.value)
)
}
/** @internal */
export const invalidate = (
self_: RcRef.RcRef
): Effect => {
const self = self_ as RcRefImpl
return core.uninterruptible(core.suspend(() => {
if (self.state._tag !== "Acquired") {
return core.void
}
const state = self.state
self.state = stateEmpty
return state.scope.close(core.exitVoid)
}))
}