/* Copyright 2025 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import { catchError, from, map, type Observable, of, startWith } from "rxjs"; import { Behavior } from "./Behavior"; /** * Data that may need to be loaded asynchronously. * * This type is for when you need to represent the current state of an operation * involving Promises as **immutable data**. See the async$ function below. */ export type Async = | { state: "loading" } | { state: "error"; value: Error } | { state: "ready"; value: A }; export const loading: Async = { state: "loading" }; export function error(value: Error): Async { return { state: "error", value }; } export function ready(value: A): Async { return { state: "ready", value }; } /** * Turn a Promise into an Observable async value. The Observable will have the * value "loading" while the Promise is pending, "ready" when the Promise * resolves, and "error" when the Promise rejects. */ export function async$(promise: Promise): Observable> { return from(promise).pipe( map(ready), startWith(loading), catchError((e: unknown) => of(error((e as Error) ?? new Error("Unknown error"))), ), ); } /** * If the async value is ready, apply the given function to the inner value. */ export function mapAsync( async: Async, project: (value: A) => B, ): Async { return async.state === "ready" ? ready(project(async.value)) : async; } export function unwrapAsync(fallback: A): (async: Async) => A { return (async: Async) => { return async.state === "ready" ? async.value : fallback; }; }