Files
effect-view/packages/effect-fc-next/src/ScopeRegistry.ts
T
Julien Valverdé 3516ba05f8
Lint / lint (push) Failing after 42s
Fix
2026-07-23 01:30:29 +02:00

204 lines
7.4 KiB
TypeScript

import { type Cause, Context, DateTime, type Duration, Effect, Equal, Exit, HashMap, Layer, Option, Order, Scope, Semaphore, Stream, SubscriptionRef } from "effect"
export interface ScopeRegistryService {
readonly ref: SubscriptionRef.SubscriptionRef<HashMap.HashMap<ScopeRegistryService.Key, ScopeRegistryService.Entry>>
register(
key: ScopeRegistryService.Key,
options: ScopeRegistryService.RegisterOptions,
): Effect.Effect<ScopeRegistryService.Entry>
commit(key: ScopeRegistryService.Key): Effect.Effect<ScopeRegistryService.Entry, Cause.NoSuchElementError>
release(key: ScopeRegistryService.Key): Effect.Effect<ScopeRegistryService.Entry, Cause.NoSuchElementError>
readonly run: Effect.Effect<void>
readonly dispose: Effect.Effect<void>
}
export declare namespace ScopeRegistryService {
export type Key = object
export interface RegisterOptions {
readonly finalizerExecutionStrategy: "sequential" | "parallel"
readonly finalizerExecutionDebounce: Duration.Input
}
export interface Entry {
readonly scope: Scope.Closeable
readonly leases: number
readonly expiresAt: Option.Option<DateTime.Utc>
readonly finalizerExecutionDebounce: Duration.Input
}
}
export const makeKey = (): ScopeRegistryService.Key => Equal.byReference({})
export class ScopeRegistryServiceImpl implements ScopeRegistryService {
constructor(
readonly ref: SubscriptionRef.SubscriptionRef<HashMap.HashMap<ScopeRegistryService.Key, ScopeRegistryService.Entry>>,
readonly runSemaphore: Semaphore.Semaphore,
) {}
register(
key: ScopeRegistryService.Key,
options: ScopeRegistryService.RegisterOptions,
): Effect.Effect<ScopeRegistryService.Entry> {
return Effect.gen({ self: this }, function*() {
const now = yield* DateTime.now
const entry = Equal.byReference<ScopeRegistryService.Entry>({
scope: yield* Scope.make(options.finalizerExecutionStrategy),
leases: 0,
expiresAt: Option.some(DateTime.addDuration(now, options.finalizerExecutionDebounce)),
finalizerExecutionDebounce: options.finalizerExecutionDebounce,
})
yield* SubscriptionRef.update(this.ref, HashMap.set(key, entry))
return entry
})
}
commit(key: ScopeRegistryService.Key): Effect.Effect<ScopeRegistryService.Entry, Cause.NoSuchElementError> {
return this.updateEntry(key, entry => Equal.byReference({
...entry,
leases: entry.leases + 1,
expiresAt: Option.none(),
}))
}
release(key: ScopeRegistryService.Key): Effect.Effect<ScopeRegistryService.Entry, Cause.NoSuchElementError> {
return Effect.flatMap(DateTime.now, now => this.updateEntry(key, entry => {
if (entry.leases === 0)
return entry
const leases = entry.leases - 1
return Equal.byReference({
...entry,
leases,
expiresAt: leases === 0
? Option.some(DateTime.addDuration(now, entry.finalizerExecutionDebounce))
: Option.none(),
})
}))
}
get run(): Effect.Effect<void> {
return this.runSemaphore.withPermits(1)(SubscriptionRef.changes(this.ref).pipe(
Stream.switchMap(entries => Option.match(this.getNextExpiration(entries), {
onNone: () => Stream.never,
onSome: expiresAt => Stream.fromEffect(DateTime.now.pipe(
Effect.flatMap(now => DateTime.isLessThan(now, expiresAt)
? Effect.sleep(DateTime.distance(now, expiresAt))
: Effect.void),
Effect.andThen(Effect.uninterruptible(this.closeExpired)),
)),
})),
Stream.runDrain,
))
}
get dispose(): Effect.Effect<void> {
return SubscriptionRef.getAndSet(
this.ref,
HashMap.empty<ScopeRegistryService.Key, ScopeRegistryService.Entry>(),
).pipe(
Effect.flatMap(entries => Effect.forEach(
HashMap.values(entries),
entry => Scope.close(entry.scope, Exit.void),
)),
Effect.asVoid,
)
}
updateEntry(
key: ScopeRegistryService.Key,
f: (entry: ScopeRegistryService.Entry) => ScopeRegistryService.Entry,
): Effect.Effect<ScopeRegistryService.Entry, Cause.NoSuchElementError> {
return SubscriptionRef.modify(this.ref, entries => {
const current = HashMap.get(entries, key)
if (current._tag === "None")
return [Option.none(), entries]
const entry = f(current.value)
return [Option.some(entry), HashMap.set(entries, key, entry)]
}).pipe(Effect.flatMap(Effect.fromOption))
}
get closeExpired(): Effect.Effect<void> {
return Effect.flatMap(DateTime.now, now => SubscriptionRef.modify(
this.ref,
entries => {
const expired: ScopeRegistryService.Entry[] = []
const remaining = HashMap.filter(entries, entry => {
const shouldClose = entry.leases === 0 && Option.exists(
entry.expiresAt,
expiresAt => DateTime.isLessThanOrEqualTo(expiresAt, now),
)
if (shouldClose)
expired.push(entry)
return !shouldClose
})
return [expired, remaining]
},
)).pipe(
Effect.flatMap(entries => Effect.forEach(
entries,
entry => Scope.close(entry.scope, Exit.void),
)),
Effect.asVoid,
)
}
getNextExpiration(
entries: HashMap.HashMap<ScopeRegistryService.Key, ScopeRegistryService.Entry>,
): Option.Option<DateTime.Utc> {
return HashMap.reduce(
entries,
Option.none<DateTime.Utc>(),
(earliest, entry) => Option.match(
Option.filter(entry.expiresAt, () => entry.leases === 0),
{
onNone: () => earliest,
onSome: expiresAt => Option.some(Option.match(earliest, {
onNone: () => expiresAt,
onSome: Order.min<DateTime.Utc>(DateTime.Order)(expiresAt),
})),
},
),
)
}
}
export const make: Effect.Effect<ScopeRegistryService> = Effect.gen(function*() {
return new ScopeRegistryServiceImpl(
yield* SubscriptionRef.make(HashMap.empty<ScopeRegistryService.Key, ScopeRegistryService.Entry>()),
yield* Semaphore.make(1),
)
})
/**
* Internal Effect service that maintains a registry of scopes associated with React component instances.
*
* This service is used internally by the `Component.useScope` hook to manage the lifecycle of component scopes,
* including tracking active scopes and coordinating their cleanup when components unmount or dependencies change.
*/
export class ScopeRegistry extends Context.Service<ScopeRegistry, ScopeRegistryService>()(
"@effect-view/ScopeRegistry/ScopeRegistry"
) {}
export const layer = Layer.effect(ScopeRegistry, Effect.tap(
make,
registry => Effect.andThen(
Effect.addFinalizer(() => registry.dispose),
Effect.forkScoped(registry.run),
),
))