1/**
2 * Helpers for turning an `Effect` program into a host application's main entry
3 * point. This module is the low-level layer used by platform adapters to run a
4 * main effect, observe its fiber, report unhandled failures, and translate the
5 * resulting `Exit` into an application or process exit code. It provides
6 * `makeRunMain`, the default teardown behavior, and error markers for custom
7 * exit codes and already-reported failures. Application code usually calls the
8 * platform-provided runner instead of using this module directly.
9 *
10 * @since 4.0.0
11 */
12import * as Cause from "effect/Cause"
13import * as Effect from "effect/Effect"
14import * as Exit from "effect/Exit"
15import { constVoid, dual } from "effect/Function"
16import type * as Fiber from "./Fiber.ts"
17
18/**
19 * Represents a teardown function that handles program completion and determines the exit code.
20 *
21 * **When to use**
22 *
23 * Use when integrating {@link makeRunMain} with a host platform that needs to
24 * translate an Effect `Exit` into a process, worker, or application exit code.
25 *
26 * **Details**
27 *
28 * A teardown function is called when an Effect program completes, either
29 * successfully or with a failure. It determines the appropriate exit code and
30 * can perform cleanup before invoking the supplied `onExit` callback.
31 *
32 * **Example** (Customizing teardown behavior)
33 *
34 * ```ts import.meta.vitest
35 * import { Effect, Exit, Runtime } from "effect"
36 *
37 * // Custom teardown that maps completion status to an exit code
38 * const customTeardown: Runtime.Teardown = (exit, onExit) => {
39 * onExit(Exit.isSuccess(exit) ? 0 : 1)
40 * }
41 *
42 * const completed = new Promise<readonly [Exit.Exit<unknown, unknown>, number]>((resolve) => {
43 * // Use with makeRunMain
44 * const runMain = Runtime.makeRunMain(({ fiber, teardown }) => {
45 * fiber.addObserver((exit) => {
46 * teardown(exit, (code) => resolve([exit, code]))
47 * })
48 * })
49 *
50 * const program = Effect.succeed("Hello, World!")
51 * runMain(program, { teardown: customTeardown })
52 * })
53 *
54 * await completed // => [Exit.succeed("Hello, World!"), 0]
55 * ```
56 *
57 * @category models
58 * @since 4.0.0
59 */
60export interface Teardown {
61 <E, A>(exit: Exit.Exit<E, A>, onExit: (code: number) => void): void
62}
63
64/**
65 * The default teardown function that determines exit codes from an Effect exit.
66 *
67 * **When to use**
68 *
69 * Use as the standard teardown for main programs with conventional process
70 * exit codes and support for {@link errorExitCode}.
71 *
72 * **Details**
73 *
74 * This teardown follows these exit-code rules:
75 *
76 * - `0` for successful completion.
77 * - `130` for interruption-only failures.
78 * - The squashed error's {@link errorExitCode} value for other failures when
79 * present.
80 * - `1` for other failures.
81 *
82 * **Gotchas**
83 *
84 * The `130` code is used only when the Cause contains interruptions and no
85 * other failure reasons. Mixed causes use the squashed error path instead.
86 *
87 * **Example** (Referencing default teardown)
88 *
89 * ```ts import.meta.vitest
90 * import { Exit, Runtime } from "effect"
91 *
92 * const exitCodes: Array<number> = []
93 * const collectExitCode = (exit: Exit.Exit<any, any>) =>
94 * Runtime.defaultTeardown(exit, (code) => exitCodes.push(code))
95 *
96 * collectExitCode(Exit.succeed(42))
97 * collectExitCode(Exit.fail("error"))
98 * collectExitCode(Exit.interrupt(123))
99 *
100 * exitCodes // => [0, 1, 130]
101 * ```
102 *
103 * @see {@link errorExitCode} for customizing failure exit codes
104 *
105 * @category running
106 * @since 4.0.0
107 */
108export const defaultTeardown: Teardown = <E, A>(
109 exit: Exit.Exit<E, A>,
110 onExit: (code: number) => void
111) => {
112 if (Exit.isSuccess(exit)) return onExit(0)
113 if (Cause.hasInterruptsOnly(exit.cause)) return onExit(130)
114 return onExit(getErrorExitCode(Cause.squash(exit.cause)))
115}
116
117/**
118 * Creates a platform-specific main program runner that handles Effect execution lifecycle.
119 *
120 * **When to use**
121 *
122 * Use when building a runtime adapter for a host platform.
123 *
124 * **Details**
125 *
126 * The runner executes Effect programs as main entry points. The provided
127 * function receives a forked fiber and a teardown callback so it can install
128 * platform-specific signal handling, fiber observers, and final exit behavior.
129 *
130 * Most applications should use a platform-provided runner, such as
131 * `NodeRuntime.runMain`, rather than constructing one directly.
132 *
133 * `disableErrorReporting` disables the automatic log emitted for unreported
134 * non-interruption failures. It does not change exit-code calculation or the
135 * custom teardown callback.
136 *
137 * **Gotchas**
138 *
139 * The setup function is responsible for observing the fiber and eventually
140 * invoking teardown. `makeRunMain` also tries to keep the host process alive
141 * with a long interval while the main fiber is running; if the host blocks
142 * timers, the runner still starts but cannot use that keep-alive fallback.
143 *
144 * **Example** (Creating platform runners)
145 *
146 * ```ts import.meta.vitest
147 * import { Effect, Exit, Runtime } from "effect"
148 *
149 * const events: Array<string> = []
150 * const completed = new Promise<readonly [Exit.Exit<unknown, unknown>, number]>((resolve) => {
151 * // Create a simple runner for a hypothetical platform
152 * const runMain = Runtime.makeRunMain(({ fiber, teardown }) => {
153 * // Handle fiber completion
154 * fiber.addObserver((exit) => {
155 * teardown(exit, (code) => resolve([exit, code]))
156 * })
157 * })
158 *
159 * // Use the runner
160 * const program = Effect.sync(() => {
161 * events.push("Starting program", "Program completed")
162 * return "success"
163 * })
164 *
165 * runMain(program, {
166 * teardown: (exit, onExit) => {
167 * events.push("Custom teardown logic")
168 * Runtime.defaultTeardown(exit, onExit)
169 * }
170 * })
171 * })
172 *
173 * const result = await completed
174 * result // => [Exit.succeed("success"), 0]
175 * events // => ["Starting program", "Program completed", "Custom teardown logic"]
176 * ```
177 *
178 * @category running
179 * @since 4.0.0
180 */
181export const makeRunMain = (
182 f: <E, A>(
183 options: {
184 readonly fiber: Fiber.Fiber<A, E>
185 readonly teardown: Teardown
186 }
187 ) => void
188): {
189 (
190 options?: {
191 readonly disableErrorReporting?: boolean | undefined
192 readonly teardown?: Teardown | undefined
193 }
194 ): <E, A>(effect: Effect.Effect<A, E>) => void
195 <E, A>(
196 effect: Effect.Effect<A, E>,
197 options?: {
198 readonly disableErrorReporting?: boolean | undefined
199 readonly teardown?: Teardown | undefined
200 }
201 ): void
202} =>
203 dual((args) => Effect.isEffect(args[0]), (effect: Effect.Effect<any, any>, options?: {
204 readonly disableErrorReporting?: boolean | undefined
205 readonly teardown?: Teardown | undefined
206 }) => {
207 const fiber = options?.disableErrorReporting === true
208 ? Effect.runFork(effect)
209 : Effect.runFork(
210 Effect.tapCause(effect, (cause) => {
211 if (Cause.hasInterruptsOnly(cause)) return Effect.void
212 const isReported = getErrorReported(Cause.squash(cause))
213 return isReported ? Effect.logError(cause) : Effect.void
214 })
215 )
216 try {
217 const keepAlive = globalThis.setInterval(constVoid, 2_147_483_647)
218 fiber.addObserver(() => {
219 clearInterval(keepAlive)
220 })
221 } catch {}
222 const teardown = options?.teardown ?? defaultTeardown
223 return f({ fiber, teardown })
224 })
225
226declare global {
227 interface Error {
228 readonly [errorExitCode]?: number
229 readonly [errorReported]?: boolean
230 }
231}
232
233/**
234 * Type-level key for the `Runtime.errorExitCode` marker.
235 *
236 * **When to use**
237 *
238 * Use to type properties keyed by `Runtime.errorExitCode` on custom error
239 * values.
240 *
241 * @category symbols
242 * @since 4.0.0
243 */
244export type errorExitCode = "~effect/Runtime/errorExitCode"
245
246/**
247 * Allows associating an exit code with an error for determining the process
248 * exit code on failure.
249 *
250 * **When to use**
251 *
252 * Use when error classes should map failures to a specific process exit code
253 * when handled by {@link defaultTeardown}.
254 *
255 * **Details**
256 *
257 * Attach this marker as a readonly property on an error object. When the main
258 * program fails, {@link defaultTeardown} squashes the Cause and reads the marker
259 * from the resulting error value.
260 *
261 * **Gotchas**
262 *
263 * The marker is read from the squashed failure value. If a Cause contains
264 * multiple failures, the selected squashed error determines the exit code.
265 *
266 * **Example** (Setting a process exit code)
267 *
268 * ```ts import.meta.vitest
269 * import { Data, Runtime } from "effect"
270 *
271 * class MyError extends Data.TaggedError("MyError") {
272 * readonly [Runtime.errorExitCode] = 42
273 * }
274 *
275 * Runtime.getErrorExitCode(new MyError()) // => 42
276 * ```
277 *
278 * @see {@link errorReported} for controlling automatic error logging
279 * @see {@link defaultTeardown} for the default failure exit-code rules that read this marker
280 * @see {@link getErrorExitCode} for reading the marker from unknown error values
281 *
282 * @category symbols
283 * @since 4.0.0
284 */
285export const errorExitCode: errorExitCode = "~effect/Runtime/errorExitCode"
286
287/**
288 * Reads the runtime exit-code marker from an unknown error value.
289 *
290 * **When to use**
291 *
292 * Use to read a custom failure exit code from an unknown error value, falling
293 * back to the default failure code.
294 *
295 * **Details**
296 *
297 * Returns the numeric `[Runtime.errorExitCode]` property when it is present on
298 * an object. Otherwise returns `1`, the default failure exit code used by
299 * `defaultTeardown`.
300 *
301 * **Gotchas**
302 *
303 * Non-object values, missing markers, and non-number marker values all return
304 * `1`.
305 *
306 * @see {@link errorExitCode} for the marker read by this function
307 *
308 * @category getters
309 * @since 4.0.0
310 */
311export const getErrorExitCode = (u: unknown): number => {
312 if (typeof u === "object" && u !== null && errorExitCode in u) {
313 const code = u[errorExitCode]
314 if (typeof code === "number") {
315 return code
316 }
317 }
318 return 1
319}
320
321/**
322 * Type-level key for the `Runtime.errorReported` marker.
323 *
324 * **When to use**
325 *
326 * Use to type properties keyed by `Runtime.errorReported` on custom error
327 * values.
328 *
329 * @category symbols
330 * @since 4.0.0
331 */
332export type errorReported = "~effect/Runtime/errorReported"
333
334/**
335 * Defines the runtime marker that controls default `runMain` error logging for an error.
336 *
337 * **When to use**
338 *
339 * Use when you need error classes reported by application code to avoid being
340 * logged again by the default main runner.
341 *
342 * **Details**
343 *
344 * Set `[Runtime.errorReported]` to `false` on an error object to suppress the
345 * runtime log because the error has already been reported. Omitted or
346 * non-boolean values are treated as `true`, so failures are logged by default.
347 *
348 * **Gotchas**
349 *
350 * This marker controls only automatic error logging. It does not change the
351 * failure Cause or the process exit code.
352 * `makeRunMain` reads the marker from `Cause.squash(cause)`, so for causes
353 * with multiple failures, the squashed error determines whether default logging
354 * is suppressed.
355 *
356 * **Example** (Suppressing error reporting)
357 *
358 * ```ts import.meta.vitest
359 * import { Data, Runtime } from "effect"
360 *
361 * class MyError extends Data.TaggedError("MyError") {
362 * readonly [Runtime.errorReported] = false
363 * }
364 *
365 * Runtime.getErrorReported(new MyError()) // => false
366 * ```
367 *
368 * @see {@link errorExitCode} for controlling failure exit codes
369 * @see {@link getErrorReported} for reading the marker from unknown error values
370 *
371 * @category symbols
372 * @since 4.0.0
373 */
374export const errorReported: errorReported = "~effect/Runtime/errorReported"
375
376/**
377 * Reads the runtime error-reporting marker from an unknown error value.
378 *
379 * **When to use**
380 *
381 * Use to read whether an unknown error value should be treated as already
382 * reported by the default main runner.
383 *
384 * **Details**
385 *
386 * Returns a boolean `[Runtime.errorReported]` property when it is present on an
387 * object. Otherwise returns `true`, so failures are logged by default.
388 *
389 * **Gotchas**
390 *
391 * Non-object values, missing markers, and non-boolean marker values all return
392 * `true`.
393 *
394 * @see {@link errorReported} for the marker read by this function
395 *
396 * @category getters
397 * @since 4.0.0
398 */
399export const getErrorReported = (u: unknown): boolean => {
400 if (typeof u === "object" && u !== null && errorReported in u) {
401 const isReported = u[errorReported]
402 if (typeof isReported === "boolean") {
403 return isReported
404 }
405 }
406 return true
407}