|
| 1 | +import{Cause,Deferred,Effect,Exit,Fiber,Option,Schema,Scope,SynchronizedRef}from"effect" |
| 2 | + |
| 3 | +exportinterfaceRunner<A,E=never>{ |
| 4 | +readonlystate: Runner.State<A,E> |
| 5 | +readonlybusy: boolean |
| 6 | +readonlyensureRunning: (work: Effect.Effect<A,E>)=>Effect.Effect<A,E> |
| 7 | +readonlystartShell: (work: (signal: AbortSignal)=>Effect.Effect<A,E>)=>Effect.Effect<A,E> |
| 8 | +readonlycancel: Effect.Effect<void> |
| 9 | +} |
| 10 | + |
| 11 | +exportnamespaceRunner{ |
| 12 | +exportclassCancelledextendsSchema.TaggedErrorClass<Cancelled>()("RunnerCancelled",{}){} |
| 13 | + |
| 14 | +interfaceRunHandle<A,E>{ |
| 15 | +id: number |
| 16 | +done: Deferred.Deferred<A,E|Cancelled> |
| 17 | +fiber: Fiber.Fiber<A,E> |
| 18 | +} |
| 19 | + |
| 20 | +interfaceShellHandle<A,E>{ |
| 21 | +id: number |
| 22 | +fiber: Fiber.Fiber<A,E> |
| 23 | +abort: AbortController |
| 24 | +} |
| 25 | + |
| 26 | +interfacePendingHandle<A,E>{ |
| 27 | +id: number |
| 28 | +done: Deferred.Deferred<A,E|Cancelled> |
| 29 | +work: Effect.Effect<A,E> |
| 30 | +} |
| 31 | + |
| 32 | +exporttypeState<A,E>= |
| 33 | +|{readonly_tag: "Idle"} |
| 34 | +|{readonly_tag: "Running";readonlyrun: RunHandle<A,E>} |
| 35 | +|{readonly_tag: "Shell";readonlyshell: ShellHandle<A,E>} |
| 36 | +|{readonly_tag: "ShellThenRun";readonlyshell: ShellHandle<A,E>;readonlyrun: PendingHandle<A,E>} |
| 37 | + |
| 38 | +exportconstmake=<A,E=never>( |
| 39 | +scope: Scope.Scope, |
| 40 | +opts?: { |
| 41 | +onIdle?: Effect.Effect<void> |
| 42 | +onBusy?: Effect.Effect<void> |
| 43 | +onInterrupt?: Effect.Effect<A,E> |
| 44 | +busy?: ()=>never |
| 45 | +}, |
| 46 | +): Runner<A,E>=>{ |
| 47 | +constref=SynchronizedRef.makeUnsafe<State<A,E>>({_tag: "Idle"}) |
| 48 | +constidle=opts?.onIdle??Effect.void |
| 49 | +constbusy=opts?.onBusy??Effect.void |
| 50 | +constonInterrupt=opts?.onInterrupt |
| 51 | +letids=0 |
| 52 | + |
| 53 | +conststate=()=>SynchronizedRef.getUnsafe(ref) |
| 54 | +constnext=()=>{ |
| 55 | +ids+=1 |
| 56 | +returnids |
| 57 | +} |
| 58 | + |
| 59 | +constcomplete=(done: Deferred.Deferred<A,E|Cancelled>,exit: Exit.Exit<A,E>)=> |
| 60 | +Exit.isFailure(exit)&&Cause.hasInterruptsOnly(exit.cause) |
| 61 | + ? Deferred.fail(done,newCancelled()).pipe(Effect.asVoid) |
| 62 | + : Deferred.done(done,exit).pipe(Effect.asVoid) |
| 63 | + |
| 64 | +constidleIfCurrent=()=> |
| 65 | +SynchronizedRef.modify(ref,(st)=>[st._tag==="Idle" ? idle : Effect.void,st]asconst).pipe(Effect.flatten) |
| 66 | + |
| 67 | +constfinishRun=(id: number,done: Deferred.Deferred<A,E|Cancelled>,exit: Exit.Exit<A,E>)=> |
| 68 | +SynchronizedRef.modify( |
| 69 | +ref, |
| 70 | +(st)=> |
| 71 | +[ |
| 72 | +Effect.gen(function*(){ |
| 73 | +if(st._tag==="Running"&&st.run.id===id)yield*idle |
| 74 | +yield*complete(done,exit) |
| 75 | +}), |
| 76 | +st._tag==="Running"&&st.run.id===id ? ({_tag: "Idle"}asconst) : st, |
| 77 | +]asconst, |
| 78 | +).pipe(Effect.flatten) |
| 79 | + |
| 80 | +conststartRun=(work: Effect.Effect<A,E>,done: Deferred.Deferred<A,E|Cancelled>)=> |
| 81 | +Effect.gen(function*(){ |
| 82 | +constid=next() |
| 83 | +constfiber=yield*work.pipe( |
| 84 | +Effect.onExit((exit)=>finishRun(id,done,exit)), |
| 85 | +Effect.forkIn(scope), |
| 86 | +) |
| 87 | +return{ id, done, fiber }satisfiesRunHandle<A,E> |
| 88 | +}) |
| 89 | + |
| 90 | +constfinishShell=(id: number)=> |
| 91 | +SynchronizedRef.modifyEffect( |
| 92 | +ref, |
| 93 | +Effect.fnUntraced(function*(st){ |
| 94 | +if(st._tag==="Shell"&&st.shell.id===id)return[idle,{_tag: "Idle"}]asconst |
| 95 | +if(st._tag==="ShellThenRun"&&st.shell.id===id){ |
| 96 | +construn=yield*startRun(st.run.work,st.run.done) |
| 97 | +return[Effect.void,{_tag: "Running", run }]asconst |
| 98 | +} |
| 99 | +return[Effect.void,st]asconst |
| 100 | +}), |
| 101 | +).pipe(Effect.flatten) |
| 102 | + |
| 103 | +conststopShell=(shell: ShellHandle<A,E>)=> |
| 104 | +Effect.gen(function*(){ |
| 105 | +shell.abort.abort() |
| 106 | +constexit=yield*Fiber.await(shell.fiber).pipe(Effect.timeoutOption("100 millis")) |
| 107 | +if(Option.isNone(exit))yield*Fiber.interrupt(shell.fiber) |
| 108 | +yield*Fiber.await(shell.fiber).pipe(Effect.exit,Effect.asVoid) |
| 109 | +}) |
| 110 | + |
| 111 | +constensureRunning=(work: Effect.Effect<A,E>)=> |
| 112 | +SynchronizedRef.modifyEffect( |
| 113 | +ref, |
| 114 | +Effect.fnUntraced(function*(st){ |
| 115 | +switch(st._tag){ |
| 116 | +case"Running": |
| 117 | +case"ShellThenRun": |
| 118 | +return[Deferred.await(st.run.done),st]asconst |
| 119 | +case"Shell": { |
| 120 | +construn={ |
| 121 | +id: next(), |
| 122 | +done: yield*Deferred.make<A,E|Cancelled>(), |
| 123 | + work, |
| 124 | +}satisfiesPendingHandle<A,E> |
| 125 | +return[Deferred.await(run.done),{_tag: "ShellThenRun",shell: st.shell, run }]asconst |
| 126 | +} |
| 127 | +case"Idle": { |
| 128 | +constdone=yield*Deferred.make<A,E|Cancelled>() |
| 129 | +construn=yield*startRun(work,done) |
| 130 | +return[Deferred.await(done),{_tag: "Running", run }]asconst |
| 131 | +} |
| 132 | +} |
| 133 | +}), |
| 134 | +).pipe( |
| 135 | +Effect.flatten, |
| 136 | +Effect.catch((e): Effect.Effect<A,E>=> |
| 137 | +einstanceofCancelled ? (onInterrupt??Effect.die(e)) : Effect.fail(easE), |
| 138 | +), |
| 139 | +) |
| 140 | + |
| 141 | +conststartShell=(work: (signal: AbortSignal)=>Effect.Effect<A,E>)=> |
| 142 | +SynchronizedRef.modifyEffect( |
| 143 | +ref, |
| 144 | +Effect.fnUntraced(function*(st){ |
| 145 | +if(st._tag!=="Idle"){ |
| 146 | +return[ |
| 147 | +Effect.sync(()=>{ |
| 148 | +if(opts?.busy)opts.busy() |
| 149 | +thrownewError("Runner is busy") |
| 150 | +}), |
| 151 | +st, |
| 152 | +]asconst |
| 153 | +} |
| 154 | +yield*busy |
| 155 | +constid=next() |
| 156 | +constabort=newAbortController() |
| 157 | +constfiber=yield*work(abort.signal).pipe(Effect.ensuring(finishShell(id)),Effect.forkChild) |
| 158 | +constshell={ id, fiber, abort }satisfiesShellHandle<A,E> |
| 159 | +return[ |
| 160 | +Effect.gen(function*(){ |
| 161 | +constexit=yield*Fiber.await(fiber) |
| 162 | +if(Exit.isSuccess(exit))returnexit.value |
| 163 | +if(Cause.hasInterruptsOnly(exit.cause)&&onInterrupt)returnyield*onInterrupt |
| 164 | +returnyield*Effect.failCause(exit.cause) |
| 165 | +}), |
| 166 | +{_tag: "Shell", shell }, |
| 167 | +]asconst |
| 168 | +}), |
| 169 | +).pipe(Effect.flatten) |
| 170 | + |
| 171 | +constcancel=SynchronizedRef.modify(ref,(st)=>{ |
| 172 | +switch(st._tag){ |
| 173 | +case"Idle": |
| 174 | +return[Effect.void,st]asconst |
| 175 | +case"Running": |
| 176 | +return[ |
| 177 | +Effect.gen(function*(){ |
| 178 | +yield*Fiber.interrupt(st.run.fiber) |
| 179 | +yield*Deferred.await(st.run.done).pipe(Effect.exit,Effect.asVoid) |
| 180 | +yield*idleIfCurrent() |
| 181 | +}), |
| 182 | +{_tag: "Idle"}asconst, |
| 183 | +]asconst |
| 184 | +case"Shell": |
| 185 | +return[ |
| 186 | +Effect.gen(function*(){ |
| 187 | +yield*stopShell(st.shell) |
| 188 | +yield*idleIfCurrent() |
| 189 | +}), |
| 190 | +{_tag: "Idle"}asconst, |
| 191 | +]asconst |
| 192 | +case"ShellThenRun": |
| 193 | +return[ |
| 194 | +Effect.gen(function*(){ |
| 195 | +yield*Deferred.fail(st.run.done,newCancelled()).pipe(Effect.asVoid) |
| 196 | +yield*stopShell(st.shell) |
| 197 | +yield*idleIfCurrent() |
| 198 | +}), |
| 199 | +{_tag: "Idle"}asconst, |
| 200 | +]asconst |
| 201 | +} |
| 202 | +}).pipe(Effect.flatten) |
| 203 | + |
| 204 | +return{ |
| 205 | +getstate(){ |
| 206 | +returnstate() |
| 207 | +}, |
| 208 | +getbusy(){ |
| 209 | +returnstate()._tag!=="Idle" |
| 210 | +}, |
| 211 | + ensureRunning, |
| 212 | + startShell, |
| 213 | + cancel, |
| 214 | +} |
| 215 | +} |
| 216 | +} |
0 commit comments