runner.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import { Cause, Deferred, Effect, Exit, Fiber, Option, Schema, Scope, SynchronizedRef } from "effect"
  2. export interface Runner<A, E = never> {
  3. readonly state: Runner.State<A, E>
  4. readonly busy: boolean
  5. readonly ensureRunning: (work: Effect.Effect<A, E>) => Effect.Effect<A, E>
  6. readonly startShell: (work: (signal: AbortSignal) => Effect.Effect<A, E>) => Effect.Effect<A, E>
  7. readonly cancel: Effect.Effect<void>
  8. }
  9. export namespace Runner {
  10. export class Cancelled extends Schema.TaggedErrorClass<Cancelled>()("RunnerCancelled", {}) {}
  11. interface RunHandle<A, E> {
  12. id: number
  13. done: Deferred.Deferred<A, E | Cancelled>
  14. fiber: Fiber.Fiber<A, E>
  15. }
  16. interface ShellHandle<A, E> {
  17. id: number
  18. fiber: Fiber.Fiber<A, E>
  19. abort: AbortController
  20. }
  21. interface PendingHandle<A, E> {
  22. id: number
  23. done: Deferred.Deferred<A, E | Cancelled>
  24. work: Effect.Effect<A, E>
  25. }
  26. export type State<A, E> =
  27. | { readonly _tag: "Idle" }
  28. | { readonly _tag: "Running"; readonly run: RunHandle<A, E> }
  29. | { readonly _tag: "Shell"; readonly shell: ShellHandle<A, E> }
  30. | { readonly _tag: "ShellThenRun"; readonly shell: ShellHandle<A, E>; readonly run: PendingHandle<A, E> }
  31. export const make = <A, E = never>(
  32. scope: Scope.Scope,
  33. opts?: {
  34. onIdle?: Effect.Effect<void>
  35. onBusy?: Effect.Effect<void>
  36. onInterrupt?: Effect.Effect<A, E>
  37. busy?: () => never
  38. },
  39. ): Runner<A, E> => {
  40. const ref = SynchronizedRef.makeUnsafe<State<A, E>>({ _tag: "Idle" })
  41. const idle = opts?.onIdle ?? Effect.void
  42. const busy = opts?.onBusy ?? Effect.void
  43. const onInterrupt = opts?.onInterrupt
  44. let ids = 0
  45. const state = () => SynchronizedRef.getUnsafe(ref)
  46. const next = () => {
  47. ids += 1
  48. return ids
  49. }
  50. const complete = (done: Deferred.Deferred<A, E | Cancelled>, exit: Exit.Exit<A, E>) =>
  51. Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)
  52. ? Deferred.fail(done, new Cancelled()).pipe(Effect.asVoid)
  53. : Deferred.done(done, exit).pipe(Effect.asVoid)
  54. const idleIfCurrent = () =>
  55. SynchronizedRef.modify(ref, (st) => [st._tag === "Idle" ? idle : Effect.void, st] as const).pipe(Effect.flatten)
  56. const finishRun = (id: number, done: Deferred.Deferred<A, E | Cancelled>, exit: Exit.Exit<A, E>) =>
  57. SynchronizedRef.modify(
  58. ref,
  59. (st) =>
  60. [
  61. Effect.gen(function* () {
  62. if (st._tag === "Running" && st.run.id === id) yield* idle
  63. yield* complete(done, exit)
  64. }),
  65. st._tag === "Running" && st.run.id === id ? ({ _tag: "Idle" } as const) : st,
  66. ] as const,
  67. ).pipe(Effect.flatten)
  68. const startRun = (work: Effect.Effect<A, E>, done: Deferred.Deferred<A, E | Cancelled>) =>
  69. Effect.gen(function* () {
  70. const id = next()
  71. const fiber = yield* work.pipe(
  72. Effect.onExit((exit) => finishRun(id, done, exit)),
  73. Effect.forkIn(scope),
  74. )
  75. return { id, done, fiber } satisfies RunHandle<A, E>
  76. })
  77. const finishShell = (id: number) =>
  78. SynchronizedRef.modifyEffect(
  79. ref,
  80. Effect.fnUntraced(function* (st) {
  81. if (st._tag === "Shell" && st.shell.id === id) return [idle, { _tag: "Idle" }] as const
  82. if (st._tag === "ShellThenRun" && st.shell.id === id) {
  83. const run = yield* startRun(st.run.work, st.run.done)
  84. return [Effect.void, { _tag: "Running", run }] as const
  85. }
  86. return [Effect.void, st] as const
  87. }),
  88. ).pipe(Effect.flatten)
  89. const stopShell = (shell: ShellHandle<A, E>) =>
  90. Effect.gen(function* () {
  91. shell.abort.abort()
  92. const exit = yield* Fiber.await(shell.fiber).pipe(Effect.timeoutOption("100 millis"))
  93. if (Option.isNone(exit)) yield* Fiber.interrupt(shell.fiber)
  94. yield* Fiber.await(shell.fiber).pipe(Effect.exit, Effect.asVoid)
  95. })
  96. const ensureRunning = (work: Effect.Effect<A, E>) =>
  97. SynchronizedRef.modifyEffect(
  98. ref,
  99. Effect.fnUntraced(function* (st) {
  100. switch (st._tag) {
  101. case "Running":
  102. case "ShellThenRun":
  103. return [Deferred.await(st.run.done), st] as const
  104. case "Shell": {
  105. const run = {
  106. id: next(),
  107. done: yield* Deferred.make<A, E | Cancelled>(),
  108. work,
  109. } satisfies PendingHandle<A, E>
  110. return [Deferred.await(run.done), { _tag: "ShellThenRun", shell: st.shell, run }] as const
  111. }
  112. case "Idle": {
  113. const done = yield* Deferred.make<A, E | Cancelled>()
  114. const run = yield* startRun(work, done)
  115. return [Deferred.await(done), { _tag: "Running", run }] as const
  116. }
  117. }
  118. }),
  119. ).pipe(
  120. Effect.flatten,
  121. Effect.catch((e): Effect.Effect<A, E> =>
  122. e instanceof Cancelled ? (onInterrupt ?? Effect.die(e)) : Effect.fail(e as E),
  123. ),
  124. )
  125. const startShell = (work: (signal: AbortSignal) => Effect.Effect<A, E>) =>
  126. SynchronizedRef.modifyEffect(
  127. ref,
  128. Effect.fnUntraced(function* (st) {
  129. if (st._tag !== "Idle") {
  130. return [
  131. Effect.sync(() => {
  132. if (opts?.busy) opts.busy()
  133. throw new Error("Runner is busy")
  134. }),
  135. st,
  136. ] as const
  137. }
  138. yield* busy
  139. const id = next()
  140. const abort = new AbortController()
  141. const fiber = yield* work(abort.signal).pipe(Effect.ensuring(finishShell(id)), Effect.forkChild)
  142. const shell = { id, fiber, abort } satisfies ShellHandle<A, E>
  143. return [
  144. Effect.gen(function* () {
  145. const exit = yield* Fiber.await(fiber)
  146. if (Exit.isSuccess(exit)) return exit.value
  147. if (Cause.hasInterruptsOnly(exit.cause) && onInterrupt) return yield* onInterrupt
  148. return yield* Effect.failCause(exit.cause)
  149. }),
  150. { _tag: "Shell", shell },
  151. ] as const
  152. }),
  153. ).pipe(Effect.flatten)
  154. const cancel = SynchronizedRef.modify(ref, (st) => {
  155. switch (st._tag) {
  156. case "Idle":
  157. return [Effect.void, st] as const
  158. case "Running":
  159. return [
  160. Effect.gen(function* () {
  161. yield* Fiber.interrupt(st.run.fiber)
  162. yield* Deferred.await(st.run.done).pipe(Effect.exit, Effect.asVoid)
  163. yield* idleIfCurrent()
  164. }),
  165. { _tag: "Idle" } as const,
  166. ] as const
  167. case "Shell":
  168. return [
  169. Effect.gen(function* () {
  170. yield* stopShell(st.shell)
  171. yield* idleIfCurrent()
  172. }),
  173. { _tag: "Idle" } as const,
  174. ] as const
  175. case "ShellThenRun":
  176. return [
  177. Effect.gen(function* () {
  178. yield* Deferred.fail(st.run.done, new Cancelled()).pipe(Effect.asVoid)
  179. yield* stopShell(st.shell)
  180. yield* idleIfCurrent()
  181. }),
  182. { _tag: "Idle" } as const,
  183. ] as const
  184. }
  185. }).pipe(Effect.flatten)
  186. return {
  187. get state() {
  188. return state()
  189. },
  190. get busy() {
  191. return state()._tag !== "Idle"
  192. },
  193. ensureRunning,
  194. startShell,
  195. cancel,
  196. }
  197. }
  198. }