import * as Context from "../../Context.ts" import * as Deferred from "../../Deferred.ts" import * as Effect from "../../Effect.ts" import * as Exit from "../../Exit.ts" import * as Fiber from "../../Fiber.ts" import * as MutableHashMap from "../../MutableHashMap.ts" import * as MutableRef from "../../MutableRef.ts" import * as Scope from "../../Scope.ts" /** * @internal */ export class ResourceMap { readonly lookup: (key: K, scope: Scope.Scope) => Effect.Effect readonly entries: BackingMap readonly isClosed: MutableRef.MutableRef constructor( lookup: (key: K, scope: Scope.Scope) => Effect.Effect, entries: BackingMap, isClosed: MutableRef.MutableRef ) { this.lookup = lookup this.entries = entries this.isClosed = isClosed } static make = Effect.fnUntraced(function*(lookup: (key: K) => Effect.Effect, options?: { readonly referential?: boolean | undefined }) { const scope = yield* Effect.scope const services = yield* Effect.context() const isClosed = MutableRef.make(false) const entries: BackingMap = options?.referential ? { _tag: "Referential", map: new Map() } : { _tag: "Equal", map: MutableHashMap.empty() } yield* Scope.addFinalizerExit( scope, (exit) => { MutableRef.set(isClosed, true) return Effect.forEach(entries.map, ([key, { fiber, scope }]) => { backingDelete(entries, key) return Effect.exit(Effect.andThen(Fiber.interrupt(fiber), Scope.close(scope, exit))) }, { concurrency: "unbounded", discard: true }) } ) return new ResourceMap( (key, scope) => Effect.provide(lookup(key), Context.add(services, Scope.Scope, scope)), entries, isClosed ) }) hasUnsafe(key: K): boolean { return backingGet(this.entries, key) !== undefined } keysUnsafe(): Array { return Array.from(this.entries.map, ([key]) => key) } get(key: K): Effect.Effect { return Effect.uninterruptibleMask((restore) => { if (MutableRef.get(this.isClosed)) { return Effect.interrupt } const existing = backingGet(this.entries, key) if (existing) { return this.awaitEntry(key, existing, restore) } const entry: Entry = { scope: Scope.makeUnsafe(), deferred: Deferred.makeUnsafe(), fiber: undefined as any, awaiters: 0 } backingSet(this.entries, key, entry) // Run the lookup on a detached fiber so it is shared by every caller and // only interrupted once all of them have been interrupted. entry.fiber = Effect.runForkWith(Fiber.getCurrent()!.context)( Effect.onExit(this.lookup(key, entry.scope), (exit) => { if (exit._tag === "Success") { return Deferred.done(entry.deferred, exit) } this.deleteEntry(key, entry) return Effect.andThen( Deferred.done(entry.deferred, exit), Scope.close(entry.scope, exit) ) }) ) return this.awaitEntry(key, entry, restore) }) } private awaitEntry( key: K, entry: Entry, restore: (effect: Effect.Effect) => Effect.Effect ): Effect.Effect { if (Deferred.isDoneUnsafe(entry.deferred)) return Deferred.await(entry.deferred) entry.awaiters++ return Effect.onExit(restore(Deferred.await(entry.deferred)), () => { if (--entry.awaiters > 0 || Deferred.isDoneUnsafe(entry.deferred)) { return Effect.void } // Detach the abandoned entry first so new callers start a fresh lookup // instead of joining one that is being interrupted. this.deleteEntry(key, entry) return Fiber.interrupt(entry.fiber) }) } private deleteEntry(key: K, entry: Entry): void { // Never delete a replacement entry created after this one was removed. if (backingGet(this.entries, key) === entry) { backingDelete(this.entries, key) } } remove(key: K): Effect.Effect { return Effect.suspend(() => { const entry = backingGet(this.entries, key) if (!entry) { return Effect.void } backingDelete(this.entries, key) return Scope.close(entry.scope, Exit.void) }) } removeIgnore(key: K): Effect.Effect { return Effect.catchCause(this.remove(key), (cause) => Effect.annotateLogs(Effect.logDebug(cause), { module: "ResourceMap", method: "removeIgnore", key })) } } type BackingMap = { readonly _tag: "Equal" readonly map: MutableHashMap.MutableHashMap> } | { readonly _tag: "Referential" readonly map: Map> } type Entry = { readonly scope: Scope.Closeable readonly deferred: Deferred.Deferred // Assigned as soon as the lookup is forked. fiber: Fiber.Fiber awaiters: number } const backingGet = (map: BackingMap, key: K): Entry | undefined => { if (map._tag === "Equal") { return MutableHashMap.get(map.map, key).valueOrUndefined } return map.map.get(key) } const backingSet = (map: BackingMap, key: K, entry: Entry): void => { if (map._tag === "Equal") { MutableHashMap.set(map.map, key, entry) } else { map.map.set(key, entry) } } const backingDelete = (map: BackingMap, key: K): void => { if (map._tag === "Equal") { MutableHashMap.remove(map.map, key) } else { map.map.delete(key) } }