599 lines
18 KiB
TypeScript
599 lines
18 KiB
TypeScript
/**
|
|
* Typed request values for data loading with `Effect.request`.
|
|
*
|
|
* A request describes one logical piece of work without performing it. It
|
|
* records the success type, typed error, service requirements, and fields a
|
|
* resolver needs to complete the request. Requests are paired with
|
|
* `RequestResolver`, which performs backend-specific loading and completes each
|
|
* pending request entry with a success, failure, cause, exit, or effect.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
import type * as Cause from "./Cause.ts"
|
|
import type * as Context from "./Context.ts"
|
|
import type * as Effect from "./Effect.ts"
|
|
import * as Equal from "./Equal.ts"
|
|
import type * as Exit from "./Exit.ts"
|
|
import { dual } from "./Function.ts"
|
|
import * as core from "./internal/core.ts"
|
|
import * as internalEffect from "./internal/effect.ts"
|
|
import { hasProperty } from "./Predicate.ts"
|
|
import type * as Types from "./Types.ts"
|
|
|
|
const TypeId = "~effect/Request"
|
|
|
|
/**
|
|
* A `Request<A, E, R>` is a request from a data source for a value of type `A`
|
|
* that may fail with an `E` and have requirements of type `R`.
|
|
*
|
|
* **Example** (Defining typed requests)
|
|
*
|
|
* ```ts
|
|
* import type { Request } from "effect"
|
|
*
|
|
* // Define a request that fetches a user by ID
|
|
* interface GetUser extends Request.Request<string, Error> {
|
|
* readonly _tag: "GetUser"
|
|
* readonly id: number
|
|
* }
|
|
*
|
|
* // Define a request that fetches all users
|
|
* interface GetAllUsers extends Request.Request<ReadonlyArray<string>, Error> {
|
|
* readonly _tag: "GetAllUsers"
|
|
* }
|
|
* ```
|
|
*
|
|
* @category models
|
|
* @since 2.0.0
|
|
*/
|
|
export interface Request<out A, out E = never, out R = never> extends Variance<A, E, R> {}
|
|
|
|
/**
|
|
* Alias for any `Request`, regardless of its success, error, or service
|
|
* requirements.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use as a generic constraint for APIs that accept any request while preserving
|
|
* each concrete request's success, error, and service types.
|
|
*
|
|
* @see {@link Request} for the request interface
|
|
* @see {@link Success} for extracting a request's success type
|
|
* @see {@link Error} for extracting a request's error type
|
|
* @see {@link Services} for extracting a request's service requirements
|
|
* @see {@link Result} for the exit type produced by completing a request
|
|
*
|
|
* @category models
|
|
* @since 4.0.0
|
|
*/
|
|
export type Any = Request<any, any, any>
|
|
|
|
/**
|
|
* Variance marker carried by every `Request`.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This marker preserves the success, error, and service requirement types for
|
|
* Effect's type-level machinery. Users normally get it by extending `Request`.
|
|
*
|
|
* @category models
|
|
* @since 2.0.0
|
|
*/
|
|
export interface Variance<out A, out E, out R> {
|
|
readonly [TypeId]: {
|
|
readonly _A: Types.Covariant<A>
|
|
readonly _E: Types.Covariant<E>
|
|
readonly _R: Types.Covariant<R>
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The constructor type returned by `Request.of` and `Request.tagged`.
|
|
*
|
|
* **Details**
|
|
*
|
|
* The constructor accepts the request's data fields, excluding request variance
|
|
* fields and any fields already supplied by the constructor such as `_tag`, and
|
|
* returns a value of the request type.
|
|
*
|
|
* **Example** (Using generated request constructors)
|
|
*
|
|
* ```ts
|
|
* import { Request } from "effect"
|
|
*
|
|
* interface GetUser extends Request.Request<string, Error> {
|
|
* readonly _tag: "GetUser"
|
|
* readonly id: number
|
|
* }
|
|
*
|
|
* // Constructor type is used internally by Request.of() and Request.tagged()
|
|
* const GetUser = Request.tagged<GetUser>("GetUser")
|
|
* const userRequest = GetUser({ id: 123 })
|
|
* ```
|
|
*
|
|
* @category models
|
|
* @since 2.0.0
|
|
*/
|
|
export interface Constructor<R extends Request<any, any, any>, T extends keyof R = never> {
|
|
(args: Types.VoidIfEmpty<Types.Simplify<Omit<R, T | keyof (Variance<any, any, any>)>>>): R
|
|
}
|
|
|
|
/**
|
|
* A utility type to extract the error type from a `Request`.
|
|
*
|
|
* **Example** (Extracting a request error type)
|
|
*
|
|
* ```ts
|
|
* import type { Request } from "effect"
|
|
*
|
|
* interface GetUser extends Request.Request<string, Error> {
|
|
* readonly id: number
|
|
* }
|
|
*
|
|
* // Extract the error type from a Request using the utility
|
|
* type UserError = Request.Error<GetUser> // Error
|
|
* ```
|
|
*
|
|
* @category utility types
|
|
* @since 2.0.0
|
|
*/
|
|
export type Error<T extends Request<any, any, any>> = [T] extends [Request<infer _A, infer _E, infer _R>] ? _E : never
|
|
|
|
/**
|
|
* A utility type to extract the value type from a `Request`.
|
|
*
|
|
* **Example** (Extracting a request success type)
|
|
*
|
|
* ```ts
|
|
* import type { Request } from "effect"
|
|
*
|
|
* interface GetUser extends Request.Request<string, Error> {
|
|
* readonly _tag: "GetUser"
|
|
* readonly id: number
|
|
* }
|
|
*
|
|
* // Extract the success type from a Request using the utility
|
|
* type UserSuccess = Request.Success<GetUser> // string
|
|
* ```
|
|
*
|
|
* @category utility types
|
|
* @since 2.0.0
|
|
*/
|
|
export type Success<T extends Request<any, any, any>> = [T] extends [Request<infer _A, infer _E, infer _R>] ? _A
|
|
: never
|
|
|
|
/**
|
|
* A utility type to extract the requirements type from a `Request`.
|
|
*
|
|
* @category utility types
|
|
* @since 4.0.0
|
|
*/
|
|
export type Services<T extends Request<any, any, any>> = [T] extends [Request<infer _A, infer _E, infer _R>] ? _R
|
|
: never
|
|
|
|
/**
|
|
* A utility type to extract the result type from a `Request`.
|
|
*
|
|
* **Example** (Extracting a request result type)
|
|
*
|
|
* ```ts
|
|
* import type { Request } from "effect"
|
|
*
|
|
* interface GetUser extends Request.Request<string, Error> {
|
|
* readonly _tag: "GetUser"
|
|
* readonly id: number
|
|
* }
|
|
*
|
|
* // Extract the result type from a Request using the utility
|
|
* type UserResult = Request.Result<GetUser> // Exit.Exit<string, Error>
|
|
* ```
|
|
*
|
|
* @category utility types
|
|
* @since 2.0.0
|
|
*/
|
|
export type Result<T extends Request<any, any, any>> = T extends Request<infer A, infer E, infer _R> ? Exit.Exit<A, E>
|
|
: never
|
|
|
|
const requestVariance = Equal.byReferenceUnsafe({
|
|
/* c8 ignore next */
|
|
_E: (_: never) => _,
|
|
/* c8 ignore next */
|
|
_A: (_: never) => _,
|
|
/* c8 ignore next */
|
|
_R: (_: never) => _
|
|
})
|
|
|
|
/**
|
|
* Prototype used by Effect's request constructors.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This low-level value provides the structural request marker for values
|
|
* created by `Request.of`, `Request.tagged`, `Request.Class`, and
|
|
* `Request.TaggedClass`. Most users should use those constructors instead of
|
|
* interacting with the prototype directly.
|
|
*
|
|
* @category prototypes
|
|
* @since 4.0.0
|
|
*/
|
|
export const RequestPrototype: Request<any, any, any> = {
|
|
...core.StructuralProto,
|
|
[TypeId]: requestVariance
|
|
}
|
|
|
|
/**
|
|
* Checks whether a value is a `Request`.
|
|
*
|
|
* **Example** (Checking request values)
|
|
*
|
|
* ```ts
|
|
* import { Request } from "effect"
|
|
*
|
|
* declare const User: unique symbol
|
|
* declare const UserNotFound: unique symbol
|
|
* type User = typeof User
|
|
* type UserNotFound = typeof UserNotFound
|
|
*
|
|
* interface GetUser extends Request.Request<User, UserNotFound> {
|
|
* readonly _tag: "GetUser"
|
|
* readonly id: string
|
|
* }
|
|
* const GetUser = Request.tagged<GetUser>("GetUser")
|
|
*
|
|
* const request = GetUser({ id: "123" })
|
|
* console.log(Request.isRequest(request)) // true
|
|
* console.log(Request.isRequest("not a request")) // false
|
|
* ```
|
|
*
|
|
* @category guards
|
|
* @since 2.0.0
|
|
*/
|
|
export const isRequest = (u: unknown): u is Request<unknown, unknown, unknown> => hasProperty(u, TypeId)
|
|
|
|
/**
|
|
* Creates a constructor function for a specific Request type.
|
|
*
|
|
* **Example** (Creating untagged request constructors)
|
|
*
|
|
* ```ts
|
|
* import { Request } from "effect"
|
|
*
|
|
* declare const UserProfile: unique symbol
|
|
* declare const ProfileError: unique symbol
|
|
* type UserProfile = typeof UserProfile
|
|
* type ProfileError = typeof ProfileError
|
|
*
|
|
* interface GetUserProfile extends Request.Request<UserProfile, ProfileError> {
|
|
* readonly id: string
|
|
* readonly includeSettings: boolean
|
|
* }
|
|
*
|
|
* const GetUserProfile = Request.of<GetUserProfile>()
|
|
*
|
|
* const request = GetUserProfile({
|
|
* id: "user-123",
|
|
* includeSettings: true
|
|
* })
|
|
* ```
|
|
*
|
|
* @category constructors
|
|
* @since 2.0.0
|
|
*/
|
|
export const of = <R extends Request<any, any, any>>(): Constructor<R> => (args) =>
|
|
Object.assign(Object.create(RequestPrototype), args)
|
|
|
|
/**
|
|
* Creates a constructor function for a tagged Request type. The tag is automatically
|
|
* added to the request, making it useful for discriminated unions.
|
|
*
|
|
* **Example** (Creating tagged request constructors)
|
|
*
|
|
* ```ts
|
|
* import { Request } from "effect"
|
|
*
|
|
* declare const User: unique symbol
|
|
* declare const UserNotFound: unique symbol
|
|
* declare const Post: unique symbol
|
|
* declare const PostNotFound: unique symbol
|
|
* type User = typeof User
|
|
* type UserNotFound = typeof UserNotFound
|
|
* type Post = typeof Post
|
|
* type PostNotFound = typeof PostNotFound
|
|
*
|
|
* interface GetUser extends Request.Request<User, UserNotFound> {
|
|
* readonly _tag: "GetUser"
|
|
* readonly id: string
|
|
* }
|
|
*
|
|
* interface GetPost extends Request.Request<Post, PostNotFound> {
|
|
* readonly _tag: "GetPost"
|
|
* readonly id: string
|
|
* }
|
|
*
|
|
* const GetUser = Request.tagged<GetUser>("GetUser")
|
|
* const GetPost = Request.tagged<GetPost>("GetPost")
|
|
*
|
|
* const userRequest = GetUser({ id: "user-123" })
|
|
* const postRequest = GetPost({ id: "post-456" })
|
|
*
|
|
* // _tag is automatically set
|
|
* console.log(userRequest._tag) // "GetUser"
|
|
* console.log(postRequest._tag) // "GetPost"
|
|
* ```
|
|
*
|
|
* @category constructors
|
|
* @since 2.0.0
|
|
*/
|
|
export const tagged = <R extends Request<any, any, any> & { _tag: string }>(
|
|
tag: R["_tag"]
|
|
): Constructor<R, "_tag"> =>
|
|
(args) => {
|
|
const request = Object.create(RequestPrototype)
|
|
if (args) Object.assign(request, args)
|
|
request._tag = tag
|
|
return request
|
|
}
|
|
|
|
/**
|
|
* Defines request types with TypeScript classes.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Subclasses pass their data fields to `super`, and instances are marked as
|
|
* `Request` values while retaining the provided readonly fields.
|
|
*
|
|
* **Example** (Defining request classes)
|
|
*
|
|
* ```ts
|
|
* import { Request } from "effect"
|
|
*
|
|
* class GetUser extends Request.Class<{ id: number }, string, Error> {
|
|
* constructor(readonly id: number) {
|
|
* super({ id })
|
|
* }
|
|
* }
|
|
*
|
|
* const getUserRequest = new GetUser(123)
|
|
* console.log(getUserRequest.id) // 123
|
|
* ```
|
|
*
|
|
* @category constructors
|
|
* @since 2.0.0
|
|
*/
|
|
export const Class: new<A extends Record<string, any>, Success, Error = never, Context = never>(
|
|
args: Types.Equals<Omit<A, keyof Request<unknown, unknown>>, {}> extends true ? void
|
|
: { readonly [P in keyof A as P extends keyof Request<any, any, any> ? never : P]: A[P] }
|
|
) => Request<Success, Error, Context> & Readonly<A> = (function() {
|
|
function Class(this: any, args: any) {
|
|
if (args) {
|
|
Object.assign(this, args)
|
|
}
|
|
}
|
|
Class.prototype = RequestPrototype
|
|
return Class as any
|
|
})()
|
|
|
|
/**
|
|
* Creates a class constructor for requests with a fixed `_tag` field.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Use this when defining class-based request types that should participate in
|
|
* tagged unions or tag-based request resolvers.
|
|
*
|
|
* **Example** (Defining tagged request classes)
|
|
*
|
|
* ```ts
|
|
* import { Request } from "effect"
|
|
*
|
|
* class GetUserById
|
|
* extends Request.TaggedClass("GetUserById")<{ id: number }, string, Error>
|
|
* {}
|
|
*
|
|
* const request = new GetUserById({ id: 123 })
|
|
* console.log(request._tag) // "GetUserById"
|
|
* console.log(request.id) // 123
|
|
* ```
|
|
*
|
|
* @category constructors
|
|
* @since 2.0.0
|
|
*/
|
|
export const TaggedClass = <Tag extends string>(
|
|
tag: Tag
|
|
): new<A extends Record<string, any>, Success, Error = never, Services = never>(
|
|
args: Types.Equals<Omit<A, keyof Request<unknown, unknown>>, {}> extends true ? void
|
|
: { readonly [P in keyof A as P extends "_tag" | keyof Request<any, any, any> ? never : P]: A[P] }
|
|
) => Request<Success, Error, Services> & Readonly<A> & { readonly _tag: Tag } => {
|
|
return class TaggedClass extends Class<any, any, any> {
|
|
readonly _tag = tag
|
|
} as any
|
|
}
|
|
|
|
/**
|
|
* Completes a request entry with the provided result.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use when you need to finish a `Request.Entry` with a prebuilt final `Exit`
|
|
* result.
|
|
*
|
|
* @see {@link completeEffect} for completing an entry from an effect that may succeed or fail
|
|
* @see {@link succeed} for completing an entry with a successful value
|
|
* @see {@link fail} for completing an entry with a typed failure
|
|
* @see {@link failCause} for completing an entry with a failure `Cause`
|
|
*
|
|
* @category completion
|
|
* @since 2.0.0
|
|
*/
|
|
export const complete: {
|
|
<A extends Any>(result: Result<A>): (self: Entry<A>) => Effect.Effect<void>
|
|
<A extends Any>(self: Entry<A>, result: Result<A>): Effect.Effect<void>
|
|
} = dual(
|
|
2,
|
|
<A extends Any>(self: Entry<A>, result: Result<A>): Effect.Effect<void> =>
|
|
internalEffect.sync(() => self.completeUnsafe(result))
|
|
)
|
|
|
|
/**
|
|
* Completes a request entry with the result of an effect.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to finish a `Request.Entry` by running an effect whose success or typed
|
|
* failure should become the request result.
|
|
*
|
|
* **Details**
|
|
*
|
|
* If the effect succeeds, the entry is completed successfully with its value.
|
|
* If the effect fails, the entry is completed with that failure.
|
|
*
|
|
* **Gotchas**
|
|
*
|
|
* The returned effect itself does not fail with the request error.
|
|
*
|
|
* @see {@link complete} for completing an entry with a prebuilt `Exit`
|
|
* @see {@link succeed} for completing an entry with a successful value
|
|
* @see {@link fail} for completing an entry with a typed failure
|
|
* @see {@link failCause} for completing an entry with a failure `Cause`
|
|
*
|
|
* @category completion
|
|
* @since 2.0.0
|
|
*/
|
|
export const completeEffect: {
|
|
<A extends Any, R>(effect: Effect.Effect<Success<A>, Error<A>, R>): (self: Entry<A>) => Effect.Effect<void, never, R>
|
|
<A extends Any, R>(self: Entry<A>, effect: Effect.Effect<Success<A>, Error<A>, R>): Effect.Effect<void, never, R>
|
|
} = dual(
|
|
2,
|
|
<A extends Any, R>(self: Entry<A>, effect: Effect.Effect<Success<A>, Error<A>, R>): Effect.Effect<void, never, R> =>
|
|
internalEffect.matchEffect(effect, {
|
|
onFailure: (error) => complete(self, core.exitFail(error) as any),
|
|
onSuccess: (value) => complete(self, core.exitSucceed(value) as any)
|
|
})
|
|
)
|
|
|
|
/**
|
|
* Completes a request entry with a typed failure.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to report a request-specific typed error while implementing a
|
|
* `RequestResolver`.
|
|
*
|
|
* @see {@link failCause} for completing an entry with a full `Cause`
|
|
* @see {@link complete} for completing an entry with an existing `Exit`
|
|
* @see {@link completeEffect} for completing an entry from an effect result
|
|
* @see {@link succeed} for completing an entry successfully
|
|
*
|
|
* @category completion
|
|
* @since 2.0.0
|
|
*/
|
|
export const fail: {
|
|
<A extends Any>(error: Error<A>): (self: Entry<A>) => Effect.Effect<void>
|
|
<A extends Any>(self: Entry<A>, error: Error<A>): Effect.Effect<void>
|
|
} = dual(
|
|
2,
|
|
<A extends Any>(self: Entry<A>, error: Error<A>): Effect.Effect<void> => complete(self, core.exitFail(error) as any)
|
|
)
|
|
|
|
/**
|
|
* Completes a request entry with a failure `Cause`.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use when you need a `RequestResolver` to complete an entry with structured
|
|
* cause information rather than only the request's typed error value.
|
|
*
|
|
* @see {@link fail} for completing an entry with a typed error value
|
|
* @see {@link complete} for completing an entry with an existing `Exit`
|
|
* @see {@link completeEffect} for completing an entry from an effect result
|
|
* @see {@link succeed} for completing an entry successfully
|
|
*
|
|
* @category completion
|
|
* @since 2.0.0
|
|
*/
|
|
export const failCause: {
|
|
<A extends Any>(cause: Cause.Cause<Error<A>>): (self: Entry<A>) => Effect.Effect<void>
|
|
<A extends Any>(self: Entry<A>, cause: Cause.Cause<Error<A>>): Effect.Effect<void>
|
|
} = dual(
|
|
2,
|
|
<A extends Any>(self: Entry<A>, cause: Cause.Cause<Error<A>>): Effect.Effect<void> =>
|
|
complete(self, core.exitFailCause(cause) as any)
|
|
)
|
|
|
|
/**
|
|
* Completes a request entry successfully with the supplied value.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use when you need to finish a `Request.Entry` with a successful request
|
|
* value.
|
|
*
|
|
* @see {@link complete} for completing an entry with a prebuilt `Exit`
|
|
* @see {@link completeEffect} for completing an entry from an effect result
|
|
* @see {@link fail} for completing an entry with a typed failure
|
|
* @see {@link failCause} for completing an entry with a failure `Cause`
|
|
*
|
|
* @category completion
|
|
* @since 2.0.0
|
|
*/
|
|
export const succeed: {
|
|
<A extends Any>(value: Success<A>): (self: Entry<A>) => Effect.Effect<void>
|
|
<A extends Any>(self: Entry<A>, value: Success<A>): Effect.Effect<void>
|
|
} = dual(
|
|
2,
|
|
<A extends Any>(self: Entry<A>, value: Success<A>): Effect.Effect<void> =>
|
|
complete(self, core.exitSucceed(value) as any)
|
|
)
|
|
|
|
/**
|
|
* A pending request handed to a `RequestResolver`.
|
|
*
|
|
* **Details**
|
|
*
|
|
* An entry contains the original request, the fiber context needed to run it,
|
|
* an `uninterruptible` flag used by batching and caching internals, and the
|
|
* `completeUnsafe` callback used by resolvers to supply the final `Exit`.
|
|
*
|
|
* @category entry
|
|
* @since 2.0.0
|
|
*/
|
|
export interface Entry<out R> {
|
|
readonly request: R
|
|
readonly context: Context.Context<
|
|
[R] extends [Request<infer _A, infer _E, infer _R>] ? _R : never
|
|
>
|
|
uninterruptible: boolean
|
|
completeUnsafe(
|
|
exit: Exit.Exit<
|
|
[R] extends [Request<infer _A, infer _E, infer _R>] ? _A : never,
|
|
[R] extends [Request<infer _A, infer _E, infer _R>] ? _E : never
|
|
>
|
|
): void
|
|
}
|
|
|
|
/**
|
|
* Creates a `Request.Entry` from its component fields.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This is a low-level helper for request runtime and resolver infrastructure;
|
|
* most application code receives entries from a `RequestResolver` instead of
|
|
* constructing them directly.
|
|
*
|
|
* @category entry
|
|
* @since 2.0.0
|
|
*/
|
|
export const makeEntry = <R>(options: {
|
|
readonly request: R
|
|
readonly context: Context.Context<
|
|
[R] extends [Request<infer _A, infer _E, infer _R>] ? _R : never
|
|
>
|
|
readonly uninterruptible: boolean
|
|
readonly completeUnsafe: (
|
|
exit: Exit.Exit<
|
|
[R] extends [Request<infer _A, infer _E, infer _R>] ? _A : never,
|
|
[R] extends [Request<infer _A, infer _E, infer _R>] ? _E : never
|
|
>
|
|
) => void
|
|
}): Entry<R> => options
|