import { SessionID } from "cojson"; import { CoValueLoadingState, ItemsSym, TypeSym } from "../internal.js"; import { type Account } from "./account.js"; import { CoFeedEntry } from "./coFeed.js"; import { type CoKeys } from "./coMap.js"; import { type CoValue, type ID } from "./interfaces.js"; /** * Returns a boolean for whether the given type is a union. * * Taken from https://github.com/sindresorhus/type-fest/blob/main/source/is-union.d.ts */ type IsUnion = ( [T] extends [never] ? false : T extends any ? [U] extends [T] ? false : true : never ) extends infer Result ? boolean extends Result ? true : Result : never; /** * A CoValue that may or may not be loaded. */ // T should extend CoValue. We can't enforce this because it would introduce circularity // into the definition of CoValues. export type MaybeLoaded = T | NotLoaded; /** * A CoValue that is either successfully loaded or that could not be loaded. */ export type Settled = T | Inaccessible; /** * A CoValue that is not loaded. */ // Manually inlining the type to reduce type-checking complexity // type NotLoaded = Loading | Inaccessible; export type NotLoaded = { $jazz: { id: ID; loadingState: | typeof CoValueLoadingState.LOADING | typeof CoValueLoadingState.DELETED | typeof CoValueLoadingState.UNAVAILABLE | typeof CoValueLoadingState.UNAUTHORIZED; }; $isLoaded: false; }; /** * A CoValue that is being loaded */ export type Loading = { $jazz: { id: ID; loadingState: typeof CoValueLoadingState.LOADING; }; $isLoaded: false; }; /** * A CoValue that could not be loaded */ export type Inaccessible = { $jazz: { id: ID; loadingState: | typeof CoValueLoadingState.DELETED | typeof CoValueLoadingState.UNAVAILABLE | typeof CoValueLoadingState.UNAUTHORIZED; }; $isLoaded: false; }; /** * Narrows a maybe-loaded, optional CoValue to a loaded and required CoValue. */ export type LoadedAndRequired = Exclude | undefined>; /** * Narrows a maybe-loaded, optional CoValue to a loaded and optional CoValue */ export type AsLoaded = Exclude>; /** * By default, if a nested CoValue is not loaded, the parent CoValue will not be loaded either. * When `$onError: "catch"` is used, the parent CoValue will always be loaded, and an {@link NotLoaded} * value will be returned for the nested CoValue if it cannot be loaded. * * Use `$onError` to handle cases where some data you have requested is inaccessible, * similar to a `try...catch` block in your query. */ type OnError = { $onError?: "catch" }; export type RefsToResolve< V, DepthLimit extends number = 10, CurrentDepth extends number[] = [], > = | boolean | (DepthLimit extends CurrentDepth["length"] ? // eslint-disable-next-line @typescript-eslint/no-explicit-any any : // Basically V extends CoList - but if we used that we'd introduce circularity into the definition of CoList itself V extends ReadonlyArray ? LoadedAndRequired extends CoValue ? | ({ $each?: RefsToResolve< AsLoaded, DepthLimit, [0, ...CurrentDepth] >; } & OnError) | boolean : OnError | boolean : // Basically V extends CoMap | Group | Account - but if we used that we'd introduce circularity into the definition of CoMap itself V extends { [TypeSym]: "CoMap" | "Group" | "Account" } ? | ({ [Key in CoKeys as LoadedAndRequired extends CoValue ? Key : never]?: RefsToResolve< LoadedAndRequired, DepthLimit, [0, ...CurrentDepth] >; } & OnError) | (ItemsSym extends keyof V ? { $each: RefsToResolve< LoadedAndRequired, DepthLimit, [0, ...CurrentDepth] >; } & OnError : never) | boolean : V extends { [TypeSym]: "CoStream"; byMe: CoFeedEntry | undefined; } ? | ({ $each: RefsToResolve< AsLoaded, DepthLimit, [0, ...CurrentDepth] >; } & OnError) | boolean : V extends { [TypeSym]: "CoPlainText" | "BinaryCoStream" } ? boolean | OnError : V extends { [TypeSym]: "SnapshotRef"; ref: infer Item; } ? | ({ ref: RefsToResolve< AsLoaded, DepthLimit, [0, ...CurrentDepth] >; } & OnError) | boolean : boolean); export type RefsToResolveStrict = [V] extends [RefsToResolve] ? RefsToResolve : V; export type Resolved< T, R extends RefsToResolve | undefined = true, > = DeeplyLoaded; /** * If the resolve query contains `$onError: "catch"`, we return a not loaded value for this nested CoValue. * Otherwise, the whole load operation returns a not-loaded value. */ type OnErrorResolvedValue = Depth extends { $onError: "catch" } ? NotLoaded : never; type CoMapLikeLoaded< V extends object, Depth, DepthLimit extends number, CurrentDepth extends number[], > = IsUnion> extends true ? // Trigger conditional type distributivity to deeply resolve each member of the union separately // Otherwise, deeply loaded values will resolve to `never` V extends V ? CoMapLikeLoaded< V, Pick, DepthLimit, CurrentDepth > : never : { readonly [Key in keyof Omit]-?: Key extends CoKeys ? LoadedAndRequired extends CoValue ? | DeeplyLoaded< LoadedAndRequired, Depth[Key], DepthLimit, [0, ...CurrentDepth] > | (undefined extends V[Key] ? undefined : never) | OnErrorResolvedValue : never : never; } & V; export type DeeplyLoaded< V, Depth, DepthLimit extends number = 10, CurrentDepth extends number[] = [], > = DepthLimit extends CurrentDepth["length"] ? V : Depth extends true | undefined ? V : // Basically V extends CoList - but if we used that we'd introduce circularity into the definition of CoList itself [V] extends [ReadonlyArray] ? // `& {}` forces TypeScript to simplify the type before performing the `extends CoValue` check. // Without it, the check would fail even when it should succeed. AsLoaded extends CoValue ? Depth extends { $each: infer ItemDepth } ? // Deeply loaded CoList ReadonlyArray< | DeeplyLoaded< AsLoaded, ItemDepth, DepthLimit, [0, ...CurrentDepth] > | OnErrorResolvedValue, Depth["$each"]> > & V // the CoList base type needs to be intersected after so that built-in methods return the correct narrowed array type : never : V : // Basically V extends CoMap | Group | Account - but if we used that we'd introduce circularity into the definition of CoMap itself [V] extends [{ [TypeSym]: "CoMap" | "Group" | "Account" }] ? // If Depth = {} return V in any case keyof Depth extends never ? V : // 1. Record-like CoMap ItemsSym extends keyof V ? // 1.1. Deeply loaded Record-like CoMap with { $each: true | { $onError: 'catch' } } Depth extends { $each: infer ItemDepth } ? { readonly [key: string]: | DeeplyLoaded< LoadedAndRequired, ItemDepth, DepthLimit, [0, ...CurrentDepth] > | OnErrorResolvedValue< LoadedAndRequired, Depth["$each"] >; } & V // same reason as in CoList : // 1.2. Deeply loaded Record-like CoMap with { [key: string]: true } string extends keyof Depth ? // if at least one key is `string`, then we treat the resolve as it was empty DeeplyLoaded & V : // 1.3 Deeply loaded Record-like CoMap with single keys CoMapLikeLoaded : // 2. Deeply loaded CoMap CoMapLikeLoaded : [V] extends [ { [TypeSym]: "SnapshotRef"; ref: infer Item; }, ] ? Depth extends { ref: infer ItemDepth } ? { readonly ref: | DeeplyLoaded< AsLoaded, ItemDepth, DepthLimit, [0, ...CurrentDepth] > | OnErrorResolvedValue, ItemDepth>; } & V : V : [V] extends [ { [TypeSym]: "CoStream"; byMe: CoFeedEntry | undefined; }, ] ? // Deeply loaded CoStream { byMe?: { value: AsLoaded }; inCurrentSession?: { value: AsLoaded }; perSession: { [key: SessionID]: { value: AsLoaded }; }; } & { [key: ID]: { value: AsLoaded } } & V // same reason as in CoList : [V] extends [ { [TypeSym]: "BinaryCoStream"; }, ] ? V : [V] extends [ { [TypeSym]: "CoPlainText"; }, ] ? V : never;