import type { NonEmptyArray } from "../Array.ts"
import * as Context from "../Context.ts"
import type { Effect } from "../Effect.ts"
import type { Exit } from "../Exit.ts"
import type { Fiber } from "../Fiber.ts"
import { dual } from "../Function.ts"
import * as MutableHashMap from "../MutableHashMap.ts"
import * as Option from "../Option.ts"
import type * as Request from "../Request.ts"
import { makeEntry } from "../Request.ts"
import type { RequestResolver } from "../RequestResolver.ts"
import { Scheduler } from "../Scheduler.ts"
import { exitDie, isEffect, withFiber } from "./core.ts"
import * as effect from "./effect.ts"
/** @internal */
export const request: {
(
resolver: RequestResolver | Effect, EX, RX>
): (self: A) => Effect<
Request.Success,
Request.Error | EX,
Request.Services | RX
>
(
self: A,
resolver: RequestResolver | Effect, EX, RX>
): Effect<
Request.Success,
Request.Error | EX,
Request.Services | RX
>
} = dual(
2,
(
self: A,
resolver: RequestResolver | Effect, EX, RX>
): Effect<
Request.Success,
Request.Error | EX,
Request.Services | RX
> => {
const withResolver = (resolver: RequestResolver) =>
effect.callback<
Request.Success,
Request.Error,
Request.Services
>((resume) => {
const entry = addEntry(resolver, self, resume, effect.getCurrentFiber()!)
return maybeRemoveEntry(resolver, entry)
})
return isEffect(resolver) ? effect.flatMap(resolver, withResolver) : withResolver(resolver)
}
)
/** @internal */
export const requestUnsafe = (
self: A,
options: {
readonly resolver: RequestResolver
readonly onExit: (exit: Exit, Request.Error>) => void
readonly context: Context.Context
}
): () => void => {
const entry = addEntry(options.resolver, self, options.onExit, {
context: options.context,
cache: { scheduler: Context.get(options.context, Scheduler) }
})
return () => removeEntryUnsafe(options.resolver, entry)
}
/**
* Pending batches of resolvers whose `batchKey` is in this set match keys with
* `Equal` instead of by reference.
*
* @internal
*/
export const equalBatchKeys = new WeakSet()
interface BatchMap {
get(key: unknown): Batch | undefined
has(key: unknown): boolean
set(key: unknown, batch: Batch): void
delete(key: unknown): void
}
const makeBatchMap = (resolver: RequestResolver): BatchMap => {
if (!equalBatchKeys.has(resolver.batchKey)) return new Map()
const map = MutableHashMap.empty()
return {
get: (key) => Option.getOrUndefined(MutableHashMap.get(map, key)),
has: (key) => MutableHashMap.has(map, key),
set: (key, batch) => MutableHashMap.set(map, key, batch),
delete: (key) => MutableHashMap.remove(map, key)
}
}
interface Batch {
key: unknown
context: Context.Context
resolver: RequestResolver
map: BatchMap
readonly entrySet: Set>
readonly entries: Set>
readonly delayEffect: Effect
readonly run: Effect
fiber?: Fiber | undefined
}
const batchPool: Array = []
const pendingBatches = new WeakMap, BatchMap>()
const addEntry = (
resolver: RequestResolver,
request: A,
resume: (exit: Exit) => void,
fiber: {
readonly context: Context.Context
readonly cache: { readonly scheduler: Scheduler }
readonly id?: number
}
) => {
let batchMap = pendingBatches.get(resolver)
if (!batchMap) {
batchMap = makeBatchMap(resolver)
pendingBatches.set(resolver, batchMap)
}
let batch: Batch | undefined
let completed = false
const entry = makeEntry({
request,
context: fiber.context as any,
uninterruptible: false,
completeUnsafe(effect) {
if (completed) return
completed = true
// Removed entries still notify resolver hooks, but not their cancelled callers.
if (batch && !batch.entrySet.delete(entry)) return
resume(effect)
}
})
if (resolver.preCheck !== undefined && !resolver.preCheck(entry)) {
return entry
}
const key = resolver.batchKey(entry)
batch = batchMap.get(key)
const isNewBatch = batch === undefined
if (!batch) {
if (batchPool.length > 0) {
batch = batchPool.pop()!
batch.key = key
batch.context = fiber.context
batch.resolver = resolver
batch.map = batchMap
} else {
const newBatch: Batch = {
key,
context: fiber.context,
resolver,
map: batchMap,
entrySet: new Set(),
entries: new Set(),
delayEffect: effect.flatMap(
withFiber((delayFiber) => {
// Reentrant delay callbacks may fill the batch and interrupt this fiber.
newBatch.fiber = delayFiber
return newBatch.resolver.delay
}),
(_) => runBatch(newBatch)
) as Effect,
run: effect.onExit(
effect.suspend(() =>
newBatch.resolver.runAll(Array.from(newBatch.entries) as NonEmptyArray>, newBatch.key)
),
(exit) => {
for (const entry of newBatch.entrySet) {
entry.completeUnsafe(
exit._tag === "Success"
? exitDie(
new Error("Effect.request: RequestResolver did not complete request", { cause: entry.request })
)
: exit
)
}
newBatch.entries.clear()
if (batchPool.length < 128) {
newBatch.entrySet.clear()
newBatch.key = undefined
newBatch.context = undefined as any
newBatch.fiber = undefined
newBatch.resolver = undefined as any
newBatch.map = undefined as any
batchPool.push(newBatch)
}
return effect.void
}
)
}
batch = newBatch
}
batchMap.set(key, batch)
}
batch.entrySet.add(entry)
batch.entries.add(entry)
if (isNewBatch) {
const delayFiber = effect.runForkWith(fiber.context)(batch.delayEffect, { scheduler: fiber.cache.scheduler })
// A synchronous delay may have already run and recycled this batch.
if (completed || batchMap.get(key) !== batch) return entry
batch.fiber = delayFiber
}
if (batch.resolver.collectWhile(batch.entries)) return entry
batch.fiber!.interruptUnsafe(fiber.id)
effect.runForkWith(batch.context)(runBatch(batch), { scheduler: Context.get(batch.context, Scheduler) })
return entry
}
const removeEntryUnsafe = (
resolver: RequestResolver,
entry: Request.Entry
) => {
if (entry.uninterruptible) return
const batchMap = pendingBatches.get(resolver)
if (!batchMap) return
const key = resolver.batchKey(entry)
const batch = batchMap.get(key)
if (!batch) return
if (!batch.entries.delete(entry)) return
batch.entrySet.delete(entry)
let fiber: Fiber | undefined
if (batch.entries.size === 0) {
batchMap.delete(key)
fiber = batch.fiber
}
// Delay finalizers may enqueue new requests, so complete the removed entry first.
entry.completeUnsafe(effect.exitInterrupt())
fiber?.interruptUnsafe()
}
const maybeRemoveEntry = (
resolver: RequestResolver,
entry: Request.Entry
) => effect.sync(() => removeEntryUnsafe(resolver, entry))
function runBatch(batch: Batch) {
if (!batch.map.has(batch.key)) return effect.void
batch.map.delete(batch.key)
return batch.run
}