EdgeAIG's picture
download
raw
103 kB
/**
* Reactive state primitives for values managed by an `AtomRegistry`.
*
* An `Atom` describes how to produce or update one piece of reactive state. The
* registry runs atom reads, remembers current values, tracks dependencies
* between atoms, starts effects and streams, and cleans up atoms that are no
* longer used. This module includes the atom constructors and update helpers
* used for cached values, effect-backed values, streams, browser state, stored
* values, and server-rendered values.
*
* @since 4.0.0
*/
import * as Arr from "../../Array.ts"
import * as Cause from "../../Cause.ts"
import * as Channel from "../../Channel.ts"
import * as Context from "../../Context.ts"
import * as Duration from "../../Duration.ts"
import * as Effect from "../../Effect.ts"
import * as Exit from "../../Exit.ts"
import * as Fiber from "../../Fiber.ts"
import type { LazyArg } from "../../Function.ts"
import { constant, constTrue, constVoid, dual, pipe } from "../../Function.ts"
import type * as Inspectable from "../../Inspectable.ts"
import { PipeInspectableProto } from "../../internal/core.ts"
import * as Layer from "../../Layer.ts"
import * as MutableHashMap from "../../MutableHashMap.ts"
import * as Option from "../../Option.ts"
import type { Pipeable } from "../../Pipeable.ts"
import { hasProperty } from "../../Predicate.ts"
import * as Pull from "../../Pull.ts"
import type { ReadonlyRecord } from "../../Record.ts"
import * as Scheduler from "../../Scheduler.ts"
import * as Schema from "../../Schema.ts"
import * as Scope from "../../Scope.ts"
import * as Stream from "../../Stream.ts"
import * as SubscriptionRef from "../../SubscriptionRef.ts"
import type { Mutable, NoInfer } from "../../Types.ts"
import * as KeyValueStore from "../persistence/KeyValueStore.ts"
import * as AsyncResult from "./AsyncResult.ts"
import { AtomRegistry } from "./AtomRegistry.ts"
import * as Registry from "./AtomRegistry.ts"
import * as Reactivity from "./Reactivity.ts"
/**
* Type-level identifier used to recognize `Atom` values.
*
* @category type IDs
* @since 4.0.0
*/
export type TypeId = "~effect/reactivity/Atom"
/**
* Runtime identifier attached to `Atom` values and used by `isAtom`.
*
* @category type IDs
* @since 4.0.0
*/
export const TypeId: TypeId = "~effect/reactivity/Atom"
/**
* Reactive value read by an `AtomRegistry`, with metadata controlling caching, laziness, refresh behavior, and initial value targeting.
*
* @category models
* @since 4.0.0
*/
export interface Atom<A> extends Pipeable, Inspectable.Inspectable {
readonly [TypeId]: TypeId
readonly keepAlive: boolean
readonly lazy: boolean
readonly read: (get: AtomContext) => A
readonly refresh?: (f: <A>(atom: Atom<A>) => void) => void
readonly label?: readonly [name: string, stack: string]
readonly idleTTL?: number
readonly initialValueTarget?: Atom<A>
}
/**
* Returns `true` when a value is an `Atom`.
*
* @category guards
* @since 4.0.0
*/
export const isAtom = (u: unknown): u is Atom<any> => hasProperty(u, TypeId)
/**
* Extracts the value type produced by an `Atom`.
*
* @category utility types
* @since 4.0.0
*/
export type Type<T extends Atom<any>> = T extends Atom<infer A> ? A : never
/**
* Extracts the success value type from an atom whose value is an `AsyncResult`.
*
* @category utility types
* @since 4.0.0
*/
export type Success<T extends Atom<any>> = T extends Atom<AsyncResult.AsyncResult<infer A, infer _>> ? A : never
/**
* Extracts the item type from an atom whose value is a `PullResult`.
*
* @category utility types
* @since 4.0.0
*/
export type PullSuccess<T extends Atom<any>> = T extends Atom<PullResult<infer A, infer _>> ? A : never
/**
* Extracts the failure error type from an atom whose value is an `AsyncResult`.
*
* @category utility types
* @since 4.0.0
*/
export type Failure<T extends Atom<any>> = T extends Atom<AsyncResult.AsyncResult<infer _, infer E>> ? E : never
/**
* Returns an atom type without serializable metadata, preserving `Writable` read and write types when the input atom is writable.
*
* @category utility types
* @since 4.0.0
*/
export type WithoutSerializable<T extends Atom<any>> = T extends Writable<infer R, infer W> ? Writable<R, W>
: Atom<Type<T>>
/**
* Runtime identifier attached to writable atoms and used by `isWritable`.
*
* @category type IDs
* @since 4.0.0
*/
export const WritableTypeId: WritableTypeId = "~effect/reactivity/Atom/Writable"
/**
* Type-level identifier used to recognize writable atoms.
*
* @category type IDs
* @since 4.0.0
*/
export type WritableTypeId = "~effect/reactivity/Atom/Writable"
/**
* Atom that can also be written to, using a `WriteContext` and an input value to update reactive state.
*
* @category models
* @since 4.0.0
*/
export interface Writable<R, W = R> extends Atom<R> {
readonly [WritableTypeId]: WritableTypeId
readonly write: (ctx: WriteContext<R>, value: W) => void
}
/**
* Context passed to atom read functions for reading dependencies, awaiting `AsyncResult` or `Option` values, managing subscriptions and finalizers, refreshing atoms, and updating writable atoms.
*
* @category context
* @since 4.0.0
*/
export interface AtomContext {
<A>(atom: Atom<A>): A
get<A>(this: AtomContext, atom: Atom<A>): A
result<A, E>(this: AtomContext, atom: Atom<AsyncResult.AsyncResult<A, E>>, options?: {
readonly suspendOnWaiting?: boolean | undefined
}): Effect.Effect<A, E>
resultOnce<A, E>(this: AtomContext, atom: Atom<AsyncResult.AsyncResult<A, E>>, options?: {
readonly suspendOnWaiting?: boolean | undefined
}): Effect.Effect<A, E>
once<A>(this: AtomContext, atom: Atom<A>): A
addFinalizer(this: AtomContext, f: () => void): void
mount<A>(this: AtomContext, atom: Atom<A>): void
refresh<A>(this: AtomContext, atom: Atom<A>): void
refreshSelf(this: AtomContext): void
self<A>(this: AtomContext): Option.Option<A>
setSelf<A>(this: AtomContext, a: A): void
set<R, W>(this: AtomContext, atom: Writable<R, W>, value: W): void
setResult<A, E, W>(this: AtomContext, atom: Writable<AsyncResult.AsyncResult<A, E>, W>, value: W): Effect.Effect<A, E>
some<A>(this: AtomContext, atom: Atom<Option.Option<A>>): Effect.Effect<A>
someOnce<A>(this: AtomContext, atom: Atom<Option.Option<A>>): Effect.Effect<A>
stream<A>(this: AtomContext, atom: Atom<A>, options?: {
readonly withoutInitialValue?: boolean
readonly bufferSize?: number
}): Stream.Stream<A>
streamResult<A, E>(this: AtomContext, atom: Atom<AsyncResult.AsyncResult<A, E>>, options?: {
readonly withoutInitialValue?: boolean
readonly bufferSize?: number
}): Stream.Stream<A, E>
subscribe<A>(this: AtomContext, atom: Atom<A>, f: (_: A) => void, options?: {
readonly immediate?: boolean
}): void
readonly registry: Registry.AtomRegistry
}
/**
* Context passed to writable atom write functions for reading atoms, refreshing or setting the current atom, and writing to other writable atoms.
*
* @category context
* @since 4.0.0
*/
export interface WriteContext<A> {
get<T>(this: WriteContext<A>, atom: Atom<T>): T
refreshSelf(this: WriteContext<A>): void
setSelf(this: WriteContext<A>, a: A): void
set<R, W>(this: WriteContext<A>, atom: Writable<R, W>, value: W): void
}
/**
* Returns a copy of an atom with an idle time-to-live: finite durations dispose it after inactivity, while an infinite duration keeps it alive.
*
* @category combinators
* @since 4.0.0
*/
export const setIdleTTL: {
/**
* Returns a copy of an atom with an idle time-to-live: finite durations dispose it after inactivity, while an infinite duration keeps it alive.
*
* @category combinators
* @since 4.0.0
*/
(duration: Duration.Input): <A extends Atom<any>>(self: A) => A
/**
* Returns a copy of an atom with an idle time-to-live: finite durations dispose it after inactivity, while an infinite duration keeps it alive.
*
* @category combinators
* @since 4.0.0
*/
<A extends Atom<any>>(self: A, duration: Duration.Input): A
} = dual<
/**
* Returns a copy of an atom with an idle time-to-live: finite durations dispose it after inactivity, while an infinite duration keeps it alive.
*
* @category combinators
* @since 4.0.0
*/
(duration: Duration.Input) => <A extends Atom<any>>(self: A) => A,
/**
* Returns a copy of an atom with an idle time-to-live: finite durations dispose it after inactivity, while an infinite duration keeps it alive.
*
* @category combinators
* @since 4.0.0
*/
<A extends Atom<any>>(self: A, duration: Duration.Input) => A
>(2, (self, durationInput) => {
const duration = Duration.fromInputUnsafe(durationInput)
const isFinite = Duration.isFinite(duration)
return Object.assign(Object.create(Object.getPrototypeOf(self)), {
...self,
keepAlive: !isFinite,
idleTTL: isFinite ? Duration.toMillis(duration) : undefined
})
})
const removeTtl = setIdleTTL(0)
const AtomProto = {
[TypeId]: TypeId,
...PipeInspectableProto,
toJSON(this: Atom<any>) {
return {
_id: "Atom",
keepAlive: this.keepAlive,
lazy: this.lazy,
label: this.label
}
}
} as const
const RuntimeProto = {
...AtomProto,
atom(this: AtomRuntime<any, any>, arg: any, options?: {
readonly initialValue?: unknown
readonly uninterruptible?: boolean | undefined
}) {
const read = makeRead(arg, options)
return readable((get) => {
const previous = get.self<AsyncResult.AsyncResult<any, any>>()
const runtimeResult = get(this)
if (runtimeResult._tag !== "Success") {
return AsyncResult.replacePrevious(runtimeResult, previous)
}
return read(get, runtimeResult.value)
})
},
fn(this: AtomRuntime<any, any>, arg: any, options?: {
readonly initialValue?: unknown
readonly reactivityKeys?: ReadonlyArray<unknown> | ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
readonly concurrent?: boolean | undefined
}) {
if (arguments.length === 0) {
return (arg: any, options?: {}) => makeFnRuntime(this, arg, options)
}
return makeFnRuntime(this, arg, options)
},
pull(this: AtomRuntime<any, any>, arg: any, options?: {
readonly disableAccumulation?: boolean
readonly initialValue?: ReadonlyArray<any>
}) {
const pullSignal = removeTtl(state(0))
const pullAtom = readable((get) => {
const previous = get.self<AsyncResult.AsyncResult<any, any>>()
const runtimeResult = get(this)
if (runtimeResult._tag !== "Success") {
return AsyncResult.replacePrevious(runtimeResult, previous)
}
return makeEffect(
get,
makeStreamPullEffect(get, pullSignal, arg, options),
AsyncResult.initial(true),
runtimeResult.value
)
})
return makeStreamPull(pullSignal, pullAtom)
},
subscriptionRef(this: AtomRuntime<any, any>, ref: any) {
return makeSubRef(
removeTtl(readable((get) => {
const previous = get.self<AsyncResult.AsyncResult<any, any>>()
const runtimeResult = get(this)
if (runtimeResult._tag !== "Success") {
return AsyncResult.replacePrevious(runtimeResult, previous)
}
const value = typeof ref === "function" ? ref(get) : ref
return SubscriptionRef.isSubscriptionRef(value)
? value
: makeEffect(get, value, AsyncResult.initial(true), runtimeResult.value)
})),
(get, ref) => {
const runtime = AsyncResult.getOrThrow(get(this))
return readSubscriptionRef(get, ref, runtime)
}
)
}
}
const makeFnRuntime = (
self: AtomRuntime<any, any>,
arg: (
arg: any,
get: FnContext
) =>
| Effect.Effect<any, any, Scope.Scope | AtomRegistry>
| Stream.Stream<any, any, AtomRegistry>,
options?: {
readonly initialValue?: unknown
readonly reactivityKeys?: ReadonlyArray<unknown> | ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
}
) => {
const [read, write, argAtom] = makeResultFn(
options?.reactivityKeys ?
((a: any, get: FnContext) => {
const effect = arg(a, get)
return Effect.isEffect(effect)
? Reactivity.mutation(effect, options.reactivityKeys!)
: Stream.ensuring(effect, Reactivity.invalidate(options.reactivityKeys!))
}) as any :
arg,
options
)
return writable((get) => {
get.get(argAtom)
const previous = get.self<AsyncResult.AsyncResult<any, any>>()
const runtimeResult = get.get(self)
if (runtimeResult._tag !== "Success") {
return AsyncResult.replacePrevious(runtimeResult, previous)
}
return read(get, runtimeResult.value)
}, write)
}
const WritableProto = {
...AtomProto,
[WritableTypeId]: WritableTypeId
} as const
/**
* Returns `true` when an atom is writable.
*
* @category refinements
* @since 4.0.0
*/
export const isWritable = <R, W>(atom: Atom<R>): atom is Writable<R, W> => WritableTypeId in atom
/**
* Creates a read-only atom from a read function and an optional custom refresh registration callback.
*
* @category constructors
* @since 4.0.0
*/
export const readable = <A>(
read: (get: AtomContext) => A,
refresh?: (f: <A>(atom: Atom<A>) => void) => void
): Atom<A> => {
const self = Object.create(AtomProto)
self.keepAlive = false
self.lazy = true
self.read = read
self.refresh = refresh
return self
}
/**
* Creates a writable atom from read and write functions, with an optional custom refresh registration callback.
*
* @category constructors
* @since 4.0.0
*/
export const writable = <R, W>(
read: (get: AtomContext) => R,
write: (ctx: WriteContext<R>, value: W) => void,
refresh?: (f: <A>(atom: Atom<A>) => void) => void
): Writable<R, W> => {
const self = Object.create(WritableProto)
self.keepAlive = false
self.lazy = true
self.read = read
self.write = write
self.refresh = refresh
return self
}
function constSetSelf<A>(ctx: WriteContext<A>, value: A) {
ctx.setSelf(value)
}
// -----------------------------------------------------------------------------
// constructors
// -----------------------------------------------------------------------------
/**
* Creates an atom from a synchronous value or read function, or from an `Effect` or `Stream` whose state is exposed as an `AsyncResult`; plain values create writable state atoms.
*
* @category constructors
* @since 4.0.0
*/
export const make: {
// -----------------------------------------------------------------------------
// constructors
// -----------------------------------------------------------------------------
/**
* Creates an atom from a synchronous value or read function, or from an `Effect` or `Stream` whose state is exposed as an `AsyncResult`; plain values create writable state atoms.
*
* @category constructors
* @since 4.0.0
*/
<A, E>(
create: (get: AtomContext) => Effect.Effect<A, E, Scope.Scope | AtomRegistry>,
options?: {
readonly initialValue?: A | undefined
readonly uninterruptible?: boolean | undefined
}
): Atom<AsyncResult.AsyncResult<A, E>>
// -----------------------------------------------------------------------------
// constructors
// -----------------------------------------------------------------------------
/**
* Creates an atom from a synchronous value or read function, or from an `Effect` or `Stream` whose state is exposed as an `AsyncResult`; plain values create writable state atoms.
*
* @category constructors
* @since 4.0.0
*/
<A, E>(
effect: Effect.Effect<A, E, Scope.Scope | AtomRegistry>,
options?: {
readonly initialValue?: A
readonly uninterruptible?: boolean | undefined
}
): Atom<AsyncResult.AsyncResult<A, E>>
// -----------------------------------------------------------------------------
// constructors
// -----------------------------------------------------------------------------
/**
* Creates an atom from a synchronous value or read function, or from an `Effect` or `Stream` whose state is exposed as an `AsyncResult`; plain values create writable state atoms.
*
* @category constructors
* @since 4.0.0
*/
<A, E>(
create: (get: AtomContext) => Stream.Stream<A, E, AtomRegistry>,
options?: {
readonly initialValue?: A
}
): Atom<AsyncResult.AsyncResult<A, E | Cause.NoSuchElementError>>
// -----------------------------------------------------------------------------
// constructors
// -----------------------------------------------------------------------------
/**
* Creates an atom from a synchronous value or read function, or from an `Effect` or `Stream` whose state is exposed as an `AsyncResult`; plain values create writable state atoms.
*
* @category constructors
* @since 4.0.0
*/
<A, E>(
stream: Stream.Stream<A, E, AtomRegistry>,
options?: {
readonly initialValue?: A
}
): Atom<AsyncResult.AsyncResult<A, E | Cause.NoSuchElementError>>
// -----------------------------------------------------------------------------
// constructors
// -----------------------------------------------------------------------------
/**
* Creates an atom from a synchronous value or read function, or from an `Effect` or `Stream` whose state is exposed as an `AsyncResult`; plain values create writable state atoms.
*
* @category constructors
* @since 4.0.0
*/
<A>(create: (get: AtomContext) => A): Atom<A>
// -----------------------------------------------------------------------------
// constructors
// -----------------------------------------------------------------------------
/**
* Creates an atom from a synchronous value or read function, or from an `Effect` or `Stream` whose state is exposed as an `AsyncResult`; plain values create writable state atoms.
*
* @category constructors
* @since 4.0.0
*/
<A>(initialValue: A): Writable<A>
} = (arg: any, options?: {
readonly initialValue?: unknown
readonly uninterruptible?: boolean | undefined
}) => {
const readOrAtom = makeRead(arg, options)
if (TypeId in readOrAtom) {
return readOrAtom as any
}
return readable(readOrAtom)
}
// -----------------------------------------------------------------------------
// constructors - effect
// -----------------------------------------------------------------------------
const makeRead: {
<A, E>(effect: Effect.Effect<A, E, Scope.Scope | AtomRegistry>, options?: {
readonly initialValue?: A
readonly uninterruptible?: boolean | undefined
}): (get: AtomContext, services?: Context.Context<any>) => AsyncResult.AsyncResult<A, E>
<A, E>(create: (get: AtomContext) => Effect.Effect<A, E, Scope.Scope | AtomRegistry>, options?: {
readonly initialValue?: A
readonly uninterruptible?: boolean | undefined
}): (get: AtomContext, services?: Context.Context<any>) => AsyncResult.AsyncResult<A, E>
<A, E>(stream: Stream.Stream<A, E, AtomRegistry>, options?: {
readonly initialValue?: A
readonly uninterruptible?: boolean | undefined
}): (get: AtomContext, services?: Context.Context<any>) => AsyncResult.AsyncResult<A, E | Cause.NoSuchElementError>
<A, E>(create: (get: AtomContext) => Stream.Stream<A, E, AtomRegistry>, options?: {
readonly initialValue?: A
readonly uninterruptible?: boolean | undefined
}): (get: AtomContext, services?: Context.Context<any>) => AsyncResult.AsyncResult<A, E | Cause.NoSuchElementError>
<A>(create: (get: AtomContext) => A): (get: AtomContext, services?: Context.Context<any>) => A
<A>(initialValue: A): Writable<A>
} = <A, E>(
arg:
| Effect.Effect<A, E, Scope.Scope | AtomRegistry>
| ((get: AtomContext) => Effect.Effect<A, E, Scope.Scope | AtomRegistry>)
| Stream.Stream<A, E, AtomRegistry>
| ((get: AtomContext) => Stream.Stream<A, E, AtomRegistry>)
| ((get: AtomContext) => A)
| A,
options?: {
readonly initialValue?: unknown
readonly uninterruptible?: boolean | undefined
}
) => {
if (typeof arg === "function" && !Effect.isEffect(arg) && !Stream.isStream(arg)) {
const create = arg as (get: AtomContext) => any
return function(get: AtomContext, providedServices?: Context.Context<any>) {
const value = create(get)
switch (typeof value) {
case "function":
case "object": {
if (value === null) return value
else if (EffectTypeId in value) {
return effect(get, value as any, options, providedServices)
} else if (StreamTypeId in value) {
return stream(get, value as any, options, providedServices)
}
return value
}
default:
return value
}
}
} else if (Effect.isEffect(arg)) {
return function(get: AtomContext, providedServices?: Context.Context<any>) {
return effect(get, arg as any, options, providedServices)
}
} else if (Stream.isStream(arg)) {
return function(get: AtomContext, providedServices?: Context.Context<any>) {
return stream(get, arg as any, options, providedServices)
}
}
return state(arg) as any
}
const EffectTypeId: keyof Effect.Effect<any> = "~effect/Effect"
const StreamTypeId: keyof Stream.Stream<any> = "~effect/Stream"
const state = <A>(
initialValue: A
): Writable<A> =>
writable(function(_get) {
return initialValue
}, constSetSelf)
const effect = <A, E>(
get: AtomContext,
effect: Effect.Effect<A, E, Scope.Scope | AtomRegistry>,
options?: {
readonly initialValue?: A
readonly uninterruptible?: boolean | undefined
},
services?: Context.Context<any>
): AsyncResult.AsyncResult<A, E> => {
const initialValue = options?.initialValue !== undefined
? AsyncResult.success<A, E>(options.initialValue)
: AsyncResult.initial<A, E>()
return makeEffect(get, effect, initialValue, services, options?.uninterruptible)
}
function makeEffect<A, E>(
ctx: AtomContext,
effect: Effect.Effect<A, E, Scope.Scope | AtomRegistry>,
initialValue: AsyncResult.AsyncResult<A, E>,
services = Context.empty(),
uninterruptible = false
): AsyncResult.AsyncResult<A, E> {
const previous = ctx.self<AsyncResult.AsyncResult<A, E>>()
const scope = Scope.makeUnsafe()
ctx.addFinalizer(() => {
Effect.runForkWith(services)(Scope.close(scope, Exit.void))
})
const servicesMap = new Map(services.mapUnsafe)
servicesMap.set(Scope.Scope.key, scope)
servicesMap.set(AtomRegistry.key, ctx.registry)
servicesMap.set(Scheduler.Scheduler.key, ctx.registry.scheduler)
let syncResult: AsyncResult.AsyncResult<A, E> | undefined
let isAsync = false
const cancel = runCallbackSync(
Context.makeUnsafe<Scope.Scope | AtomRegistry>(servicesMap),
effect,
function(exit) {
syncResult = AsyncResult.fromExitWithPrevious(exit, previous)
if (isAsync) {
ctx.setSelf(syncResult)
}
},
uninterruptible
)
isAsync = true
if (cancel !== undefined) {
ctx.addFinalizer(cancel)
}
if (syncResult !== undefined) {
return syncResult
} else if (previous._tag === "Some") {
return AsyncResult.waitingFrom(previous)
}
return AsyncResult.waiting(initialValue)
}
function runCallbackSync<R, A, E, ER = never>(
services: Context.Context<R>,
effect: Effect.Effect<A, E, R>,
onExit: (exit: Exit.Exit<A, E | ER>) => void,
uninterruptible = false
): (() => void) | undefined {
if (Exit.isExit(effect)) {
onExit(effect as any)
return undefined
}
const runFork = Effect.runForkWith(services)
const fiber = runFork(effect)
fiber.currentDispatcher?.flush()
const result = fiber.pollUnsafe()
if (result) {
onExit(result)
return undefined
}
const remove = fiber.addObserver(onExit)
function cancel() {
remove()
if (!uninterruptible) {
fiber.interruptUnsafe()
}
}
return cancel
}
// -----------------------------------------------------------------------------
// context
// -----------------------------------------------------------------------------
/**
* Atom that builds a `Context` from a `Layer` and exposes constructors for atoms, functions, pulls, and subscription refs that run with that context.
*
* @category models
* @since 4.0.0
*/
export interface AtomRuntime<R, ER = never> extends Atom<AsyncResult.AsyncResult<Context.Context<R>, ER>> {
readonly factory: RuntimeFactory
readonly layer: Atom<Layer.Layer<R, ER>>
readonly atom: {
<A, E>(
create: (get: AtomContext) => Effect.Effect<A, E, Scope.Scope | R | AtomRegistry | Reactivity.Reactivity>,
options?: {
readonly initialValue?: A
readonly uninterruptible?: boolean | undefined
}
): Atom<AsyncResult.AsyncResult<A, E | ER>>
<A, E>(effect: Effect.Effect<A, E, Scope.Scope | R | AtomRegistry | Reactivity.Reactivity>, options?: {
readonly initialValue?: A
readonly uninterruptible?: boolean | undefined
}): Atom<AsyncResult.AsyncResult<A, E | ER>>
<A, E>(create: (get: AtomContext) => Stream.Stream<A, E, AtomRegistry | Reactivity.Reactivity | R>, options?: {
readonly initialValue?: A
}): Atom<AsyncResult.AsyncResult<A, E | ER | Cause.NoSuchElementError>>
<A, E>(stream: Stream.Stream<A, E, AtomRegistry | Reactivity.Reactivity | R>, options?: {
readonly initialValue?: A
}): Atom<AsyncResult.AsyncResult<A, E | ER | Cause.NoSuchElementError>>
}
readonly fn: {
<Arg>(): {
<E, A>(
fn: (arg: Arg, get: FnContext) => Effect.Effect<A, E, Scope.Scope | AtomRegistry | Reactivity.Reactivity | R>,
options?: {
readonly initialValue?: A | undefined
readonly reactivityKeys?: ReadonlyArray<unknown> | ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
readonly concurrent?: boolean | undefined
}
): AtomResultFn<Arg, A, E | ER>
<E, A>(
fn: (arg: Arg, get: FnContext) => Stream.Stream<A, E, AtomRegistry | Reactivity.Reactivity | R>,
options?: {
readonly initialValue?: A | undefined
readonly reactivityKeys?: ReadonlyArray<unknown> | ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
readonly concurrent?: boolean | undefined
}
): AtomResultFn<Arg, A, E | ER | Cause.NoSuchElementError>
}
<E, A, Arg = void>(
fn: (arg: Arg, get: FnContext) => Effect.Effect<A, E, Scope.Scope | AtomRegistry | Reactivity.Reactivity | R>,
options?: {
readonly initialValue?: A | undefined
readonly reactivityKeys?: ReadonlyArray<unknown> | ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
readonly concurrent?: boolean | undefined
}
): AtomResultFn<Arg, A, E | ER>
<E, A, Arg = void>(
fn: (arg: Arg, get: FnContext) => Stream.Stream<A, E, AtomRegistry | Reactivity.Reactivity | R>,
options?: {
readonly initialValue?: A | undefined
readonly reactivityKeys?: ReadonlyArray<unknown> | ReadonlyRecord<string, ReadonlyArray<unknown>> | undefined
readonly concurrent?: boolean | undefined
}
): AtomResultFn<Arg, A, E | ER | Cause.NoSuchElementError>
}
readonly pull: <A, E>(
create:
| ((get: AtomContext) => Stream.Stream<A, E, R | AtomRegistry | Reactivity.Reactivity>)
| Stream.Stream<A, E, R | AtomRegistry | Reactivity.Reactivity>,
options?: {
readonly disableAccumulation?: boolean
readonly initialValue?: ReadonlyArray<A>
}
) => Writable<PullResult<A, E | ER>, void>
readonly subscriptionRef: <A, E>(
create:
| Effect.Effect<SubscriptionRef.SubscriptionRef<A>, E, Scope.Scope | R | AtomRegistry | Reactivity.Reactivity>
| ((
get: AtomContext
) => Effect.Effect<SubscriptionRef.SubscriptionRef<A>, E, Scope.Scope | R | AtomRegistry | Reactivity.Reactivity>)
) => Writable<AsyncResult.AsyncResult<A, E>, A>
}
/**
* Factory for `AtomRuntime` values that share a `Layer.MemoMap` and a set of global layers.
*
* @category models
* @since 4.0.0
*/
export interface RuntimeFactory {
<R, E>(
create:
| Layer.Layer<R, E, AtomRegistry | Reactivity.Reactivity>
| ((get: AtomContext) => Layer.Layer<R, E, AtomRegistry | Reactivity.Reactivity>)
): AtomRuntime<R, E>
readonly memoMap: Layer.MemoMap
readonly addGlobalLayer: <A, E>(layer: Layer.Layer<A, E, AtomRegistry | Reactivity.Reactivity>) => void
/**
* Uses the `Reactivity` service from the runtime to refresh the atom whenever
* the keys change.
*/
readonly withReactivity: (
keys: ReadonlyArray<unknown> | ReadonlyRecord<string, ReadonlyArray<unknown>>
) => <A extends Atom<any>>(atom: A) => A
}
/**
* Creates a `RuntimeFactory` backed by the supplied `Layer.MemoMap`.
*
* @category constructors
* @since 4.0.0
*/
export const context: (options: {
readonly memoMap: Layer.MemoMap
}) => RuntimeFactory = (options) => {
let globalLayer: Layer.Layer<any, any, AtomRegistry> = Reactivity.layer
function factory<E, R>(
create:
| Layer.Layer<R, E, AtomRegistry | Reactivity.Reactivity>
| ((get: AtomContext) => Layer.Layer<R, E, AtomRegistry | Reactivity.Reactivity>)
): AtomRuntime<R, E> {
const self = Object.create(RuntimeProto)
self.keepAlive = false
self.lazy = true
self.refresh = undefined
self.factory = factory
const layerAtom = keepAlive(
typeof create === "function"
? readable((get) => Layer.provideMerge(create(get), globalLayer))
: readable(() => Layer.provideMerge(create, globalLayer))
)
self.layer = layerAtom
self.read = function read(get: AtomContext) {
const layer = get(layerAtom)
const build = Effect.flatMap(Effect.scope, (scope) => Layer.buildWithMemoMap(layer, options.memoMap, scope))
return effect(get, build, { uninterruptible: true })
}
return self
}
factory.memoMap = options.memoMap
factory.addGlobalLayer = (layer: Layer.Layer<any, any, AtomRegistry | Reactivity.Reactivity>) => {
globalLayer = Layer.provideMerge(globalLayer, Layer.provide(layer, Reactivity.layer))
}
const reactivityAtom = removeTtl(make(
Effect.contextWith((services: Context.Context<Scope.Scope>) =>
Layer.buildWithMemoMap(Reactivity.layer, options.memoMap, Context.get(services, Scope.Scope))
).pipe(
Effect.map(Context.get(Reactivity.Reactivity))
)
))
factory.withReactivity =
(keys: ReadonlyArray<unknown> | ReadonlyRecord<string, ReadonlyArray<unknown>>) =>
<A extends Atom<any>>(atom: A): A =>
transform(atom, (get) => {
const reactivity = AsyncResult.getOrThrow(get(reactivityAtom))
get.addFinalizer(reactivity.registerUnsafe(keys, () => {
get.refresh(atom)
}))
get.subscribe(atom, (value) => get.setSelf(value))
return get.once(atom)
}, { initialValueTarget: atom }) as any as A
return factory
}
/**
* Default `Layer.MemoMap` used by the module-level `runtime` factory.
*
* @category context
* @since 4.0.0
*/
export const defaultMemoMap: Layer.MemoMap = Layer.makeMemoMapUnsafe()
/**
* Default `RuntimeFactory` created with `defaultMemoMap`.
*
* @category context
* @since 4.0.0
*/
export const runtime: RuntimeFactory = context({ memoMap: defaultMemoMap })
/**
* Returns `Rx.runtime.withReactivity` for refreshing an atom whenever the
* keys change in the `Reactivity` service.
*
* **When to use**
*
* Use to refresh an atom whenever one or more invalidation keys change in the
* default reactivity runtime.
*
* @category reactivity
* @since 4.0.0
*/
export const withReactivity: (
keys: ReadonlyArray<unknown> | ReadonlyRecord<string, ReadonlyArray<unknown>>
) => <A extends Atom<any>>(atom: A) => A = runtime.withReactivity
// -----------------------------------------------------------------------------
// constructors - stream
// -----------------------------------------------------------------------------
const stream = <A, E>(
get: AtomContext,
stream: Stream.Stream<A, E, AtomRegistry>,
options?: {
readonly initialValue?: A
},
services?: Context.Context<any>
): AsyncResult.AsyncResult<A, E | Cause.NoSuchElementError> => {
const initialValue = options?.initialValue !== undefined
? AsyncResult.success<A, E>(options.initialValue)
: AsyncResult.initial<A, E>()
return makeStream(get, stream, initialValue, services)
}
function makeStream<A, E>(
ctx: AtomContext,
stream: Stream.Stream<A, E, AtomRegistry>,
initialValue: AsyncResult.AsyncResult<A, E | Cause.NoSuchElementError>,
services = Context.empty()
): AsyncResult.AsyncResult<A, E | Cause.NoSuchElementError> {
const previous = ctx.self<AsyncResult.AsyncResult<A, E | Cause.NoSuchElementError>>()
services = Context.add(services, AtomRegistry, ctx.registry)
const run = Effect.scopedWith((scope) =>
Effect.flatMap(Channel.toPullScoped(stream.channel, scope), (pull) =>
Effect.whileLoop({
while: constTrue,
body: () => pull,
step(arr) {
ctx.setSelf(AsyncResult.success(Arr.lastNonEmpty(arr), {
waiting: true
}))
}
}))
).pipe(
Effect.catchCause((cause) => {
if (Pull.isDoneCause(cause)) {
pipe(
ctx.self<AsyncResult.AsyncResult<A, E | Cause.NoSuchElementError>>(),
Option.flatMap(AsyncResult.value),
Option.match({
onNone: () =>
ctx.setSelf(
AsyncResult.failWithPrevious(new Cause.NoSuchElementError(), {
previous: ctx.self<AsyncResult.AsyncResult<A, E | Cause.NoSuchElementError>>()
})
),
onSome: (a) => ctx.setSelf(AsyncResult.success(a))
})
)
} else {
ctx.setSelf(AsyncResult.failureWithPrevious(cause as Cause.Cause<E>, {
previous: ctx.self<AsyncResult.AsyncResult<A, E | Cause.NoSuchElementError>>()
}))
}
return Effect.void
})
)
const servicesMap = new Map(services.mapUnsafe)
servicesMap.set(AtomRegistry.key, ctx.registry)
servicesMap.set(Scheduler.Scheduler.key, ctx.registry.scheduler)
const cancel = runCallbackSync(
Context.makeUnsafe<AtomRegistry>(servicesMap),
run,
constVoid,
false
)
if (cancel !== undefined) {
ctx.addFinalizer(cancel)
}
if (previous._tag === "Some") {
return AsyncResult.waitingFrom(previous)
}
return AsyncResult.waiting(initialValue)
}
// -----------------------------------------------------------------------------
// constructors - subscription ref
// -----------------------------------------------------------------------------
/**
* Creates a writable atom backed by a `SubscriptionRef`, or by an effect that produces one, updating from ref changes and writing atom updates back to the ref.
*
* @category constructors
* @since 4.0.0
*/
export const subscriptionRef: {
// -----------------------------------------------------------------------------
// constructors - subscription ref
// -----------------------------------------------------------------------------
/**
* Creates a writable atom backed by a `SubscriptionRef`, or by an effect that produces one, updating from ref changes and writing atom updates back to the ref.
*
* @category constructors
* @since 4.0.0
*/
<A>(
ref: SubscriptionRef.SubscriptionRef<A> | ((get: AtomContext) => SubscriptionRef.SubscriptionRef<A>)
): Writable<A>
// -----------------------------------------------------------------------------
// constructors - subscription ref
// -----------------------------------------------------------------------------
/**
* Creates a writable atom backed by a `SubscriptionRef`, or by an effect that produces one, updating from ref changes and writing atom updates back to the ref.
*
* @category constructors
* @since 4.0.0
*/
<A, E>(
effect:
| Effect.Effect<SubscriptionRef.SubscriptionRef<A>, E, Scope.Scope | AtomRegistry>
| ((get: AtomContext) => Effect.Effect<SubscriptionRef.SubscriptionRef<A>, E, Scope.Scope | AtomRegistry>)
): Writable<AsyncResult.AsyncResult<A, E>, A>
} = (
ref:
| SubscriptionRef.SubscriptionRef<any>
| ((get: AtomContext) => SubscriptionRef.SubscriptionRef<any>)
| Effect.Effect<SubscriptionRef.SubscriptionRef<any>, any, Scope.Scope | AtomRegistry>
| ((get: AtomContext) => Effect.Effect<SubscriptionRef.SubscriptionRef<any>, any, Scope.Scope | AtomRegistry>)
) =>
makeSubRef(
readable((get) => {
const value = typeof ref === "function" ? ref(get) : ref
return SubscriptionRef.isSubscriptionRef(value)
? value
: makeEffect(get, value, AsyncResult.initial(true))
}),
readSubscriptionRef
) as any
const readSubscriptionRef = (
get: AtomContext,
sub:
| SubscriptionRef.SubscriptionRef<any>
| AsyncResult.AsyncResult<SubscriptionRef.SubscriptionRef<any>, any>,
services = Context.empty()
) => {
if (SubscriptionRef.isSubscriptionRef(sub)) {
get.addFinalizer(
SubscriptionRef.changes(sub).pipe(
Stream.runForEachArray((arr) => {
for (let i = 0; i < arr.length; i++) {
get.setSelf(arr[i])
}
return Effect.void
}),
Effect.runCallbackWith(services)
)
)
return Effect.runSyncWith(services)(SubscriptionRef.get(sub))
} else if (sub._tag !== "Success") {
return sub
}
return makeStream(get, SubscriptionRef.changes(sub.value), AsyncResult.initial(true), services)
}
const makeSubRef = (
refAtom: Atom<
SubscriptionRef.SubscriptionRef<any> | AsyncResult.AsyncResult<SubscriptionRef.SubscriptionRef<any>, any>
>,
read: (
get: AtomContext,
ref: SubscriptionRef.SubscriptionRef<any> | AsyncResult.Success<SubscriptionRef.SubscriptionRef<any>, any>
) => any
) => {
function write(ctx: WriteContext<SubscriptionRef.SubscriptionRef<any>>, value: any) {
const ref = ctx.get(refAtom)
if (SubscriptionRef.isSubscriptionRef(ref)) {
Effect.runSync(SubscriptionRef.set(ref, value))
} else if (AsyncResult.isSuccess(ref)) {
Effect.runSync(SubscriptionRef.set(ref.value, value))
}
}
return writable((get) => {
const ref = get(refAtom)
if (SubscriptionRef.isSubscriptionRef(ref)) {
return read(get, ref)
} else if (AsyncResult.isSuccess(ref)) {
return read(get, ref)
}
return ref
}, write)
}
// -----------------------------------------------------------------------------
// constructors - functions
// -----------------------------------------------------------------------------
/**
* Context passed to `fn` and `fnSync` computations for reading atoms, awaiting results, registering finalizers, refreshing atoms, subscribing to changes, and writing updates.
*
* @category models
* @since 4.0.0
*/
export interface FnContext {
<A>(atom: Atom<A>): A
result<A, E>(this: FnContext, atom: Atom<AsyncResult.AsyncResult<A, E>>, options?: {
readonly suspendOnWaiting?: boolean | undefined
}): Effect.Effect<A, E>
addFinalizer(this: FnContext, f: () => void): void
mount<A>(this: FnContext, atom: Atom<A>): void
refresh<A>(this: FnContext, atom: Atom<A>): void
self<A>(this: FnContext): Option.Option<A>
setSelf<A>(this: FnContext, a: A): void
set<R, W>(this: FnContext, atom: Writable<R, W>, value: W): void
setResult<A, E, W>(this: FnContext, atom: Writable<AsyncResult.AsyncResult<A, E>, W>, value: W): Effect.Effect<A, E>
some<A>(this: FnContext, atom: Atom<Option.Option<A>>): Effect.Effect<A>
stream<A>(this: FnContext, atom: Atom<A>, options?: {
readonly withoutInitialValue?: boolean
readonly bufferSize?: number
}): Stream.Stream<A>
streamResult<A, E>(this: FnContext, atom: Atom<AsyncResult.AsyncResult<A, E>>, options?: {
readonly withoutInitialValue?: boolean
readonly bufferSize?: number
}): Stream.Stream<A, E>
subscribe<A>(this: FnContext, atom: Atom<A>, f: (_: A) => void, options?: {
readonly immediate?: boolean
}): void
readonly registry: Registry.AtomRegistry
}
/**
* Creates a writable atom for a synchronous function; writing an argument re-runs the function, returning `Option.none` before the first call unless an initial value is supplied.
*
* @category constructors
* @since 4.0.0
*/
export const fnSync: {
/**
* Creates a writable atom for a synchronous function; writing an argument re-runs the function, returning `Option.none` before the first call unless an initial value is supplied.
*
* @category constructors
* @since 4.0.0
*/
<Arg>(): {
/**
* Creates a writable atom for a synchronous function; writing an argument re-runs the function, returning `Option.none` before the first call unless an initial value is supplied.
*
* @category constructors
* @since 4.0.0
*/
<A>(f: (arg: Arg, get: FnContext) => A): Writable<Option.Option<A>, Arg>
/**
* Creates a writable atom for a synchronous function; writing an argument re-runs the function, returning `Option.none` before the first call unless an initial value is supplied.
*
* @category constructors
* @since 4.0.0
*/
<A>(f: (arg: Arg, get: FnContext) => A, options: { readonly initialValue: A }): Writable<A, Arg>
}
/**
* Creates a writable atom for a synchronous function; writing an argument re-runs the function, returning `Option.none` before the first call unless an initial value is supplied.
*
* @category constructors
* @since 4.0.0
*/
<A, Arg = void>(f: (arg: Arg, get: FnContext) => A): Writable<Option.Option<A>, Arg>
/**
* Creates a writable atom for a synchronous function; writing an argument re-runs the function, returning `Option.none` before the first call unless an initial value is supplied.
*
* @category constructors
* @since 4.0.0
*/
<A, Arg = void>(f: (arg: Arg, get: FnContext) => A, options: { readonly initialValue: A }): Writable<A, Arg>
} = function(...args: ReadonlyArray<any>) {
if (args.length === 0) {
return makeFnSync
}
return makeFnSync(...args as [any, any]) as any
}
const makeFnSync = <Arg, A>(f: (arg: Arg, get: FnContext) => A, options?: {
readonly initialValue?: A
}): Writable<Option.Option<A> | A, Arg> => {
const argAtom = removeTtl(state<[number, Arg]>([0, undefined as any]))
const hasInitialValue = options?.initialValue !== undefined
return writable(function(get) {
;(get as any).isFn = true
const [counter, arg] = get.get(argAtom)
if (counter === 0) {
return hasInitialValue ? options.initialValue : Option.none()
}
return hasInitialValue ? f(arg, get) : Option.some(f(arg, get))
}, function(ctx, arg) {
batch(() => {
ctx.set(argAtom, [ctx.get(argAtom)[0] + 1, arg as Arg])
ctx.refreshSelf()
})
})
}
/**
* Writable async function atom whose value is an `AsyncResult` and whose writes accept function arguments plus `Reset` and `Interrupt` controls.
*
* @category models
* @since 4.0.0
*/
export interface AtomResultFn<Arg, A, E = never>
extends Writable<AsyncResult.AsyncResult<A, E>, Arg | Reset | Interrupt>
{}
/**
* Defines the control symbol that can be written to an `AtomResultFn` to reset it to its initial state.
*
* **When to use**
*
* Use when you need an `AtomResultFn` write value that clears the current async
* result and returns it to the initial state.
*
* @category symbols
* @since 4.0.0
*/
export const Reset = Symbol.for("effect/reactivity/atom/Atom/Reset")
/**
* Type of the `Reset` control symbol accepted by `AtomResultFn` writes.
*
* @category symbols
* @since 4.0.0
*/
export type Reset = typeof Reset
/**
* Defines the control symbol that can be written to an `AtomResultFn` to interrupt the current asynchronous computation.
*
* **When to use**
*
* Use when you need an `AtomResultFn` write value that interrupts the currently
* running async computation.
*
* @category symbols
* @since 4.0.0
*/
export const Interrupt = Symbol.for("effect/reactivity/atom/Atom/Interrupt")
/**
* Type of the `Interrupt` control symbol accepted by `AtomResultFn` writes.
*
* @category symbols
* @since 4.0.0
*/
export type Interrupt = typeof Interrupt
/**
* Creates a writable atom for an `Effect` or `Stream` function; writing an argument starts the computation and exposes its state as an `AsyncResult`.
*
* @category constructors
* @since 4.0.0
*/
export const fn: {
/**
* Creates a writable atom for an `Effect` or `Stream` function; writing an argument starts the computation and exposes its state as an `AsyncResult`.
*
* @category constructors
* @since 4.0.0
*/
<Arg>(): <E, A>(fn: (arg: Arg, get: FnContext) => Effect.Effect<A, E, Scope.Scope | AtomRegistry>, options?: {
readonly initialValue?: A | undefined
readonly concurrent?: boolean | undefined
}) => AtomResultFn<Arg, A, E>
/**
* Creates a writable atom for an `Effect` or `Stream` function; writing an argument starts the computation and exposes its state as an `AsyncResult`.
*
* @category constructors
* @since 4.0.0
*/
<E, A, Arg = void>(
fn: (arg: Arg, get: FnContext) => Effect.Effect<A, E, Scope.Scope | AtomRegistry>,
options?: {
readonly initialValue?: A | undefined
readonly concurrent?: boolean | undefined
}
): AtomResultFn<Arg, A, E>
/**
* Creates a writable atom for an `Effect` or `Stream` function; writing an argument starts the computation and exposes its state as an `AsyncResult`.
*
* @category constructors
* @since 4.0.0
*/
<Arg>(): <E, A>(fn: (arg: Arg, get: FnContext) => Stream.Stream<A, E, AtomRegistry>, options?: {
readonly initialValue?: A | undefined
readonly concurrent?: boolean | undefined
}) => AtomResultFn<Arg, A, E | Cause.NoSuchElementError>
/**
* Creates a writable atom for an `Effect` or `Stream` function; writing an argument starts the computation and exposes its state as an `AsyncResult`.
*
* @category constructors
* @since 4.0.0
*/
<E, A, Arg = void>(
fn: (arg: Arg, get: FnContext) => Stream.Stream<A, E, AtomRegistry>,
options?: {
readonly initialValue?: A | undefined
readonly concurrent?: boolean | undefined
}
): AtomResultFn<Arg, A, E | Cause.NoSuchElementError>
} = function(...args: ReadonlyArray<any>) {
if (args.length === 0) {
return makeFn
}
return makeFn(...args as [any, any]) as any
}
const makeFn = <Arg, E, A>(
f: (arg: Arg, get: FnContext) => Stream.Stream<A, E, AtomRegistry> | Effect.Effect<A, E, Scope.Scope | AtomRegistry>,
options?: {
readonly initialValue?: A | undefined
readonly concurrent?: boolean | undefined
}
): AtomResultFn<Arg, A, E | Cause.NoSuchElementError> => {
const [read, write] = makeResultFn(f, options)
return writable(read, write) as any
}
function makeResultFn<Arg, E, A>(
f: (arg: Arg, get: FnContext) => Effect.Effect<A, E, Scope.Scope | AtomRegistry> | Stream.Stream<A, E, AtomRegistry>,
options?: {
readonly initialValue?: A
readonly concurrent?: boolean | undefined
}
) {
const argAtom = removeTtl(state<[number, Arg | Interrupt]>([0, undefined as any]))
const initialValue = options?.initialValue !== undefined
? AsyncResult.success<A, E>(options.initialValue)
: AsyncResult.initial<A, E>()
const fibersAtom = options?.concurrent
? removeTtl(readable((get) => {
const fibers = new Set<Fiber.Fiber<any, any>>()
get.addFinalizer(() => fibers.forEach((f) => f.interruptUnsafe()))
return fibers
}))
: undefined
function read(
get: AtomContext,
services?: Context.Context<any>
): AsyncResult.AsyncResult<A, E | Cause.NoSuchElementError> {
const fibers = fibersAtom ? get(fibersAtom) : undefined
;(get as any).isFn = true
const [counter, arg] = get.get(argAtom)
if (counter === 0) {
return initialValue
} else if (arg === Interrupt) {
return AsyncResult.failureWithPrevious(Cause.interrupt(), { previous: get.self() })
}
let value = f(arg, get)
if (EffectTypeId in value) {
if (fibers) {
const eff = value as Effect.Effect<A, E, Scope.Scope | AtomRegistry>
value = Effect.flatMap(
Effect.forkDetach(eff, { startImmediately: true }),
(fiber) => {
fibers.add(fiber)
fiber.addObserver(() => fibers.delete(fiber))
return Effect.map(Fiber.joinAll(fibers), (arr) => arr[0])
}
)
}
return makeEffect(get, value as any, initialValue, services, false)
}
return makeStream(get, value as any, initialValue, services)
}
function write(
ctx: WriteContext<AsyncResult.AsyncResult<A, E | Cause.NoSuchElementError>>,
arg: Arg | Reset | Interrupt
) {
batch(() => {
if (arg === Reset) {
ctx.set(argAtom, [0, undefined as any])
} else if (arg === Interrupt) {
ctx.set(argAtom, [ctx.get(argAtom)[0] + 1, Interrupt])
} else {
ctx.set(argAtom, [ctx.get(argAtom)[0] + 1, arg])
}
ctx.refreshSelf()
})
}
return [read, write, argAtom] as const
}
/**
* `AsyncResult` produced by `pull`, containing a non-empty batch of pulled items and a `done` flag, or `NoSuchElementError` when the stream completes without items.
*
* @category models
* @since 4.0.0
*/
export type PullResult<A, E = never> = AsyncResult.AsyncResult<{
readonly done: boolean
readonly items: Arr.NonEmptyArray<A>
}, E | Cause.NoSuchElementError>
/**
* Creates a writable atom that pulls an initial chunk from a stream and then pulls the next chunk whenever it is written to, accumulating items unless `disableAccumulation` is enabled.
*
* @category constructors
* @since 4.0.0
*/
export const pull = <A, E>(
create: ((get: AtomContext) => Stream.Stream<A, E, AtomRegistry>) | Stream.Stream<A, E, AtomRegistry>,
options?: {
readonly disableAccumulation?: boolean | undefined
}
): Writable<PullResult<A, E>, void> => {
const pullSignal = removeTtl(state(0))
const pullAtom = readable(makeRead(function(get) {
return makeStreamPullEffect(get, pullSignal, create, options)
}))
return makeStreamPull(pullSignal, pullAtom)
}
const makeStreamPullEffect = <A, E>(
get: AtomContext,
pullSignal: Atom<number>,
create: Stream.Stream<A, E, AtomRegistry> | ((get: AtomContext) => Stream.Stream<A, E, AtomRegistry>),
options?: {
readonly disableAccumulation?: boolean | undefined
}
): Effect.Effect<
{ readonly done: boolean; readonly items: Arr.NonEmptyArray<A> },
E | Cause.NoSuchElementError,
Scope.Scope | AtomRegistry
> =>
Effect.flatMap(
Stream.toPull(typeof create === "function" ? create(get) : create),
(pullChunk) => {
const fiber = Fiber.getCurrent()!
const services = fiber.context as Context.Context<AtomRegistry | Scope.Scope>
let acc: ReadonlyArray<A> = Arr.empty<A>()
const pull: Effect.Effect<
{
done: boolean
items: Arr.NonEmptyArray<A>
},
Cause.NoSuchElementError | E,
Registry.AtomRegistry
> = Effect.matchCauseEffect(pullChunk, {
onFailure(cause): Effect.Effect<
{ done: boolean; items: Arr.NonEmptyArray<A> },
Cause.NoSuchElementError | E
> {
if (Pull.isDoneCause(cause)) {
if (!Arr.isReadonlyArrayNonEmpty(acc)) {
return Effect.fail(new Cause.NoSuchElementError(`Atom.pull: no items`))
}
return Effect.succeed({ done: true, items: acc as Arr.NonEmptyArray<A> })
}
return Effect.failCause(cause as Cause.Cause<E>)
},
onSuccess(chunk) {
let items: Arr.NonEmptyArray<A>
if (options?.disableAccumulation) {
items = chunk as any
} else {
items = Arr.appendAll(acc, chunk)
acc = items
}
return Effect.succeed({ done: false, items })
}
})
const cancels = new Set<() => void>()
get.addFinalizer(() => {
for (const cancel of cancels) cancel()
})
get.once(pullSignal)
get.subscribe(pullSignal, () => {
get.setSelf(AsyncResult.waitingFrom(get.self<PullResult<A, E>>()))
let cancel: (() => void) | undefined
// eslint-disable-next-line prefer-const
cancel = runCallbackSync(services, pull, (exit) => {
if (cancel) cancels.delete(cancel)
const result = AsyncResult.fromExitWithPrevious(exit, get.self())
const pending = cancels.size > 0
get.setSelf(pending ? AsyncResult.waiting(result) : result)
})
if (cancel) cancels.add(cancel)
})
return pull
}
)
const makeStreamPull = <A, E>(
pullSignal: Writable<number>,
pullAtom: Atom<PullResult<A, E>>
) =>
writable(pullAtom.read, function(ctx, _) {
ctx.set(pullSignal, ctx.get(pullSignal) + 1)
})
/**
* Creates a memoized atom factory that returns the same object for the same argument, using weak references for cached values when the platform supports them.
*
* @category constructors
* @since 4.0.0
*/
export const family = typeof WeakRef === "undefined" || typeof FinalizationRegistry === "undefined" ?
<Arg, T extends object>(
f: (arg: Arg) => T
): (arg: Arg) => T => {
const atoms = MutableHashMap.empty<Arg, T>()
return function(arg) {
const atomEntry = MutableHashMap.get(atoms, arg)
if (atomEntry._tag === "Some") {
return atomEntry.value
}
const newAtom = f(arg)
MutableHashMap.set(atoms, arg, newAtom)
return newAtom
}
} :
<Arg, T extends object>(
f: (arg: Arg) => T
): (arg: Arg) => T => {
const atoms = MutableHashMap.empty<Arg, WeakRef<T>>()
const registry = new FinalizationRegistry<Arg>((arg) => {
MutableHashMap.remove(atoms, arg)
})
return function(arg) {
const atomEntry = MutableHashMap.get(atoms, arg).pipe(
Option.flatMapNullishOr((ref) => ref.deref())
)
if (atomEntry._tag === "Some") {
return atomEntry.value
}
const newAtom = f(arg)
MutableHashMap.set(atoms, arg, new WeakRef(newAtom))
registry.register(newAtom, arg)
return newAtom
}
}
/**
* Uses a fallback `AsyncResult` atom while the primary atom is `Initial`, marking the fallback result as waiting until the primary atom produces a non-initial result.
*
* @category combinators
* @since 4.0.0
*/
export const withFallback: {
/**
* Uses a fallback `AsyncResult` atom while the primary atom is `Initial`, marking the fallback result as waiting until the primary atom produces a non-initial result.
*
* @category combinators
* @since 4.0.0
*/
<E2, A2>(fallback: Atom<AsyncResult.AsyncResult<A2, E2>>): <R extends Atom<AsyncResult.AsyncResult<any, any>>>(
self: R
) => [R] extends [Writable<infer _, infer RW>] ? Writable<
AsyncResult.AsyncResult<
AsyncResult.AsyncResult.Success<Type<R>> | A2,
AsyncResult.AsyncResult.Failure<Type<R>> | E2
>,
RW
>
: Atom<
AsyncResult.AsyncResult<
AsyncResult.AsyncResult.Success<Type<R>> | A2,
AsyncResult.AsyncResult.Failure<Type<R>> | E2
>
>
/**
* Uses a fallback `AsyncResult` atom while the primary atom is `Initial`, marking the fallback result as waiting until the primary atom produces a non-initial result.
*
* @category combinators
* @since 4.0.0
*/
<R extends Atom<AsyncResult.AsyncResult<any, any>>, A2, E2>(self: R, fallback: Atom<AsyncResult.AsyncResult<A2, E2>>): [R] extends [Writable<infer _, infer RW>] ? Writable<
AsyncResult.AsyncResult<
AsyncResult.AsyncResult.Success<Type<R>> | A2,
AsyncResult.AsyncResult.Failure<Type<R>> | E2
>,
RW
>
: Atom<
AsyncResult.AsyncResult<
AsyncResult.AsyncResult.Success<Type<R>> | A2,
AsyncResult.AsyncResult.Failure<Type<R>> | E2
>
>
} = dual(2, <R extends Atom<AsyncResult.AsyncResult<any, any>>, A2, E2>(
self: R,
fallback: Atom<AsyncResult.AsyncResult<A2, E2>>
): [R] extends [Writable<infer _, infer RW>] ? Writable<
AsyncResult.AsyncResult<
AsyncResult.AsyncResult.Success<Type<R>> | A2,
AsyncResult.AsyncResult.Failure<Type<R>> | E2
>,
RW
>
: Atom<
AsyncResult.AsyncResult<
AsyncResult.AsyncResult.Success<Type<R>> | A2,
AsyncResult.AsyncResult.Failure<Type<R>> | E2
>
> =>
{
function withFallback(get: AtomContext) {
const result = get(self)
if (result._tag === "Initial") {
return AsyncResult.waiting(get(fallback))
}
return result
}
return isWritable(self)
? writable(
withFallback,
self.write,
self.refresh ?? function(refresh) {
refresh(self)
}
) as any
: readable(
withFallback,
self.refresh ?? function(refresh) {
refresh(self)
}
) as any
})
/**
* Returns a copy of an atom that remains cached and mounted even when no subscribers are using it.
*
* @category combinators
* @since 4.0.0
*/
export const keepAlive = <A extends Atom<any>>(self: A): A =>
Object.assign(Object.create(Object.getPrototypeOf(self)), {
...self,
keepAlive: true
})
/**
* Allows a reactive value to be disposed of when it is not in use.
*
* **Details**
*
* Atoms have this behavior by default, so use this to undo `keepAlive` on a copied atom.
*
* @category combinators
* @since 4.0.0
*/
export const autoDispose = <A extends Atom<any>>(self: A): A =>
Object.assign(Object.create(Object.getPrototypeOf(self)), {
...self,
keepAlive: false
})
/**
* Sets whether an atom should be lazy.
*
* **Details**
*
* Lazy atoms defer recomputation while they have no active listeners or active
* non-lazy dependents, rebuilding the next time their value is observed.
*
* @category combinators
* @since 4.0.0
*/
export const setLazy: {
/**
* Sets whether an atom should be lazy.
*
* **Details**
*
* Lazy atoms defer recomputation while they have no active listeners or active
* non-lazy dependents, rebuilding the next time their value is observed.
*
* @category combinators
* @since 4.0.0
*/
(lazy: boolean): <A extends Atom<any>>(self: A) => A
/**
* Sets whether an atom should be lazy.
*
* **Details**
*
* Lazy atoms defer recomputation while they have no active listeners or active
* non-lazy dependents, rebuilding the next time their value is observed.
*
* @category combinators
* @since 4.0.0
*/
<A extends Atom<any>>(self: A, lazy: boolean): A
} = dual(2, <A extends Atom<any>>(self: A, lazy: boolean) =>
Object.assign(Object.create(Object.getPrototypeOf(self)), {
...self,
lazy
}))
/**
* Attaches a diagnostic label to an atom.
*
* **Details**
*
* The label is used for inspection and debugging metadata and does not change the
* atom's read or write behavior.
*
* @category combinators
* @since 4.0.0
*/
export const withLabel: {
/**
* Attaches a diagnostic label to an atom.
*
* **Details**
*
* The label is used for inspection and debugging metadata and does not change the
* atom's read or write behavior.
*
* @category combinators
* @since 4.0.0
*/
(name: string): <A extends Atom<any>>(self: A) => A
/**
* Attaches a diagnostic label to an atom.
*
* **Details**
*
* The label is used for inspection and debugging metadata and does not change the
* atom's read or write behavior.
*
* @category combinators
* @since 4.0.0
*/
<A extends Atom<any>>(self: A, name: string): A
} = dual<
/**
* Attaches a diagnostic label to an atom.
*
* **Details**
*
* The label is used for inspection and debugging metadata and does not change the
* atom's read or write behavior.
*
* @category combinators
* @since 4.0.0
*/
(name: string) => <A extends Atom<any>>(self: A) => A,
/**
* Attaches a diagnostic label to an atom.
*
* **Details**
*
* The label is used for inspection and debugging metadata and does not change the
* atom's read or write behavior.
*
* @category combinators
* @since 4.0.0
*/
<A extends Atom<any>>(self: A, name: string) => A
>(2, (self, name) =>
Object.assign(Object.create(Object.getPrototypeOf(self)), {
...self,
label: [name, new Error().stack?.split("\n")[5] ?? ""]
}))
/**
* Pairs an atom with an initial value for registry initialization.
*
* **When to use**
*
* Use to preload an atom value when constructing or seeding a registry.
*
* **Details**
*
* The returned tuple can be supplied to `AtomRegistry` initial values so the atom
* starts with the provided value before it is first rebuilt.
*
* @category combinators
* @since 4.0.0
*/
export const initialValue: {
/**
* Pairs an atom with an initial value for registry initialization.
*
* **When to use**
*
* Use to preload an atom value when constructing or seeding a registry.
*
* **Details**
*
* The returned tuple can be supplied to `AtomRegistry` initial values so the atom
* starts with the provided value before it is first rebuilt.
*
* @category combinators
* @since 4.0.0
*/
<A>(initialValue: A): (self: Atom<A>) => readonly [Atom<A>, A]
/**
* Pairs an atom with an initial value for registry initialization.
*
* **When to use**
*
* Use to preload an atom value when constructing or seeding a registry.
*
* **Details**
*
* The returned tuple can be supplied to `AtomRegistry` initial values so the atom
* starts with the provided value before it is first rebuilt.
*
* @category combinators
* @since 4.0.0
*/
<A>(self: Atom<A>, initialValue: A): readonly [Atom<A>, A]
} = dual<
/**
* Pairs an atom with an initial value for registry initialization.
*
* **When to use**
*
* Use to preload an atom value when constructing or seeding a registry.
*
* **Details**
*
* The returned tuple can be supplied to `AtomRegistry` initial values so the atom
* starts with the provided value before it is first rebuilt.
*
* @category combinators
* @since 4.0.0
*/
<A>(initialValue: A) => (self: Atom<A>) => readonly [Atom<A>, A],
/**
* Pairs an atom with an initial value for registry initialization.
*
* **When to use**
*
* Use to preload an atom value when constructing or seeding a registry.
*
* **Details**
*
* The returned tuple can be supplied to `AtomRegistry` initial values so the atom
* starts with the provided value before it is first rebuilt.
*
* @category combinators
* @since 4.0.0
*/
<A>(self: Atom<A>, initialValue: A) => readonly [Atom<A>, A]
>(2, (self, initialValue) => [self, initialValue])
/**
* Creates a derived atom by reading another atom with a custom `AtomContext`
* function.
*
* **Details**
*
* If the source is writable, the derived atom keeps the source write input and
* forwards writes to the source. `initialValueTarget` controls which atom receives
* preloaded initial values for the derived atom.
*
* @category combinators
* @since 4.0.0
*/
export const transform: {
/**
* Creates a derived atom by reading another atom with a custom `AtomContext`
* function.
*
* **Details**
*
* If the source is writable, the derived atom keeps the source write input and
* forwards writes to the source. `initialValueTarget` controls which atom receives
* preloaded initial values for the derived atom.
*
* @category combinators
* @since 4.0.0
*/
<R extends Atom<any>, B>(
f: (get: AtomContext, atom: R) => B,
options?: {
readonly initialValueTarget?: Atom<B> | undefined
}
): (self: R) => [R] extends [Writable<infer _, infer RW>] ? Writable<B, RW> : Atom<B>
/**
* Creates a derived atom by reading another atom with a custom `AtomContext`
* function.
*
* **Details**
*
* If the source is writable, the derived atom keeps the source write input and
* forwards writes to the source. `initialValueTarget` controls which atom receives
* preloaded initial values for the derived atom.
*
* @category combinators
* @since 4.0.0
*/
<R extends Atom<any>, B>(
self: R,
f: (get: AtomContext, atom: R) => B,
options?: {
readonly initialValueTarget?: Atom<B> | undefined
}
): [R] extends [Writable<infer _, infer RW>] ? Writable<B, RW> : Atom<B>
} = dual(
(args) => isAtom(args[0]),
(<A, B>(
self: Atom<A>,
f: (get: AtomContext, atom: Atom<A>, options?: {
readonly initialValueTarget?: Atom<B> | undefined
}) => B,
options?: {
readonly initialValueTarget?: Atom<B> | undefined
}
): Atom<B> => {
const atom = removeTtl(
isWritable(self)
? writable(
(get) => f(get, self),
function(ctx, value) {
ctx.set(self, value)
},
self.refresh ?? function(refresh) {
refresh(self)
}
)
: readable(
(get) => f(get, self),
self.refresh ?? function(refresh) {
refresh(self)
}
)
)
if (options?.initialValueTarget) {
;(atom as Mutable<Atom<B>>).initialValueTarget = getInitialValueTarget(options.initialValueTarget)
}
return atom
}) as any
)
const getInitialValueTarget = <A>(atom: Atom<A>): Atom<A> => {
let target = atom
while (target.initialValueTarget) {
target = target.initialValueTarget
}
return target
}
/**
* Maps the current value of an atom with a pure function.
*
* **Details**
*
* When the source atom is writable, the returned atom remains writable and keeps
* the source atom's write input type.
*
* @category combinators
* @since 4.0.0
*/
export const map: {
/**
* Maps the current value of an atom with a pure function.
*
* **Details**
*
* When the source atom is writable, the returned atom remains writable and keeps
* the source atom's write input type.
*
* @category combinators
* @since 4.0.0
*/
<R extends Atom<any>, B>(f: (_: Type<R>) => B): (self: R) => [R] extends [Writable<infer _, infer RW>] ? Writable<B, RW> : Atom<B>
/**
* Maps the current value of an atom with a pure function.
*
* **Details**
*
* When the source atom is writable, the returned atom remains writable and keeps
* the source atom's write input type.
*
* @category combinators
* @since 4.0.0
*/
<R extends Atom<any>, B>(self: R, f: (_: Type<R>) => B): [R] extends [Writable<infer _, infer RW>] ? Writable<B, RW> : Atom<B>
} = dual(
2,
<A, B>(self: Atom<A>, f: (_: A) => B): Atom<B> => transform(self, (get) => f(get(self)))
)
/**
* Maps the successful value inside an `AsyncResult` atom.
*
* **Details**
*
* Initial and failure states are preserved, and writable source atoms keep their
* original write input type.
*
* @category combinators
* @since 4.0.0
*/
export const mapResult: {
/**
* Maps the successful value inside an `AsyncResult` atom.
*
* **Details**
*
* Initial and failure states are preserved, and writable source atoms keep their
* original write input type.
*
* @category combinators
* @since 4.0.0
*/
<R extends Atom<AsyncResult.AsyncResult<any, any>>, B>(f: (_: AsyncResult.AsyncResult.Success<Type<R>>) => B): (
self: R
) => [R] extends [Writable<infer _, infer RW>] ?
Writable<AsyncResult.AsyncResult<B, AsyncResult.AsyncResult.Failure<Type<R>>>, RW>
: Atom<AsyncResult.AsyncResult<B, AsyncResult.AsyncResult.Failure<Type<R>>>>
/**
* Maps the successful value inside an `AsyncResult` atom.
*
* **Details**
*
* Initial and failure states are preserved, and writable source atoms keep their
* original write input type.
*
* @category combinators
* @since 4.0.0
*/
<R extends Atom<AsyncResult.AsyncResult<any, any>>, B>(self: R, f: (_: AsyncResult.AsyncResult.Success<Type<R>>) => B): [R] extends [Writable<infer _, infer RW>] ?
Writable<AsyncResult.AsyncResult<B, AsyncResult.AsyncResult.Failure<Type<R>>>, RW>
: Atom<AsyncResult.AsyncResult<B, AsyncResult.AsyncResult.Failure<Type<R>>>>
} = dual(2, <R extends Atom<AsyncResult.AsyncResult<any, any>>, B>(
self: R,
f: (_: AsyncResult.AsyncResult.Success<Type<R>>) => B
): [R] extends [Writable<infer _, infer RW>] ?
Writable<AsyncResult.AsyncResult<B, AsyncResult.AsyncResult.Failure<Type<R>>>, RW>
: Atom<AsyncResult.AsyncResult<B, AsyncResult.AsyncResult.Failure<Type<R>>>> => map(self, AsyncResult.map(f)))
/**
* Creates an atom that publishes source changes only after the source has stopped
* changing for the specified duration.
*
* **Details**
*
* The current source value is used immediately, and any pending debounce timer is
* cleared when the derived atom is disposed.
*
* @category combinators
* @since 4.0.0
*/
export const debounce: {
/**
* Creates an atom that publishes source changes only after the source has stopped
* changing for the specified duration.
*
* **Details**
*
* The current source value is used immediately, and any pending debounce timer is
* cleared when the derived atom is disposed.
*
* @category combinators
* @since 4.0.0
*/
(duration: Duration.Input): <A extends Atom<any>>(self: A) => WithoutSerializable<A>
/**
* Creates an atom that publishes source changes only after the source has stopped
* changing for the specified duration.
*
* **Details**
*
* The current source value is used immediately, and any pending debounce timer is
* cleared when the derived atom is disposed.
*
* @category combinators
* @since 4.0.0
*/
<A extends Atom<any>>(self: A, duration: Duration.Input): WithoutSerializable<A>
} = dual(
2,
<A>(self: Atom<A>, duration: Duration.Input): Atom<A> => {
const millis = Duration.toMillis(Duration.fromInputUnsafe(duration))
return transform(self, function(get) {
let timeout: number | undefined
let value = get.once(self)
function update() {
timeout = undefined
get.setSelf(value)
}
get.addFinalizer(function() {
if (timeout) clearTimeout(timeout)
})
get.subscribe(self, function(val) {
value = val
if (timeout) clearTimeout(timeout)
timeout = setTimeout(update, millis) as any
})
return value
}, { initialValueTarget: self })
}
)
/**
* Creates a derived atom that reads the source and schedules a refresh after the
* specified duration.
*
* **Details**
*
* The scheduled refresh is canceled when the derived atom's lifetime is disposed.
*
* @category combinators
* @since 4.0.0
*/
export const withRefresh: {
/**
* Creates a derived atom that reads the source and schedules a refresh after the
* specified duration.
*
* **Details**
*
* The scheduled refresh is canceled when the derived atom's lifetime is disposed.
*
* @category combinators
* @since 4.0.0
*/
(duration: Duration.Input): <A extends Atom<any>>(self: A) => WithoutSerializable<A>
/**
* Creates a derived atom that reads the source and schedules a refresh after the
* specified duration.
*
* **Details**
*
* The scheduled refresh is canceled when the derived atom's lifetime is disposed.
*
* @category combinators
* @since 4.0.0
*/
<A extends Atom<any>>(self: A, duration: Duration.Input): WithoutSerializable<A>
} = dual(
2,
<A>(self: Atom<A>, duration: Duration.Input): Atom<A> => {
const millis = Duration.toMillis(Duration.fromInputUnsafe(duration))
return transform(self, function(get) {
const handle = setTimeout(() => get.refresh(self), millis) as any
get.addFinalizer(() => clearTimeout(handle))
return get(self)
}, { initialValueTarget: self })
}
)
/**
* Adds stale-while-revalidate refresh behavior to an async result atom.
*
* **Details**
*
* Automatic revalidation during reads is skipped while the current value is
* fresh within `staleTime`. Manual `refresh` calls remain forceful and always
* forward to the wrapped atom. Use `revalidateOnMount` to control whether stale data should trigger a
* background refresh on first mount. Use `revalidateOnFocus` to control
* focus behavior. `true` respects `staleTime` and `"always"` forces refetch.
*
* @category combinators
* @since 4.0.0
*/
export const swr: {
/**
* Adds stale-while-revalidate refresh behavior to an async result atom.
*
* **Details**
*
* Automatic revalidation during reads is skipped while the current value is
* fresh within `staleTime`. Manual `refresh` calls remain forceful and always
* forward to the wrapped atom. Use `revalidateOnMount` to control whether stale data should trigger a
* background refresh on first mount. Use `revalidateOnFocus` to control
* focus behavior. `true` respects `staleTime` and `"always"` forces refetch.
*
* @category combinators
* @since 4.0.0
*/
(
options: {
readonly staleTime: Duration.Input
readonly revalidateOnMount?: boolean | undefined
readonly revalidateOnFocus?: boolean | "always" | undefined
readonly focusSignal?: Atom<any> | undefined
}
): <R extends Atom<AsyncResult.AsyncResult<any, any>>>(self: R) => WithoutSerializable<R>
/**
* Adds stale-while-revalidate refresh behavior to an async result atom.
*
* **Details**
*
* Automatic revalidation during reads is skipped while the current value is
* fresh within `staleTime`. Manual `refresh` calls remain forceful and always
* forward to the wrapped atom. Use `revalidateOnMount` to control whether stale data should trigger a
* background refresh on first mount. Use `revalidateOnFocus` to control
* focus behavior. `true` respects `staleTime` and `"always"` forces refetch.
*
* @category combinators
* @since 4.0.0
*/
<R extends Atom<AsyncResult.AsyncResult<any, any>>>(
self: R,
options: {
readonly staleTime: Duration.Input
readonly revalidateOnMount?: boolean | undefined
readonly revalidateOnFocus?: boolean | "always" | undefined
readonly focusSignal?: Atom<any> | undefined
}
): WithoutSerializable<R>
} = dual(
2,
<A, E>(
self: Atom<AsyncResult.AsyncResult<A, E>>,
options: {
readonly staleTime: Duration.Input
readonly revalidateOnMount?: boolean | undefined
readonly revalidateOnFocus?: boolean | "always" | undefined
readonly focusSignal?: Atom<any> | undefined
}
): Atom<AsyncResult.AsyncResult<A, E>> => {
const staleTime = Duration.toMillis(Duration.fromInputUnsafe(options.staleTime))
return transform(self, (get) => {
const current = get.once(self)
get.subscribe(self, (value) => {
get.setSelf(value)
})
if (options.revalidateOnFocus && options.focusSignal) {
get.once(options.focusSignal)
get.subscribe(
options.focusSignal,
options.revalidateOnFocus === "always" ? () => get.refresh(self) : () => {
const current = get.once(self)
if (shouldRevalidateSWR(current, staleTime)) {
get.refresh(self)
}
}
)
}
const firstRead = Option.isNone(get.self<AsyncResult.AsyncResult<A, E>>())
if (firstRead && options.revalidateOnMount === false) {
return current
}
if (shouldRevalidateSWR(current, staleTime)) {
get.refresh(self)
}
return current
}, { initialValueTarget: self })
}
) as any
const swrTimestamp = <A, E>(result: AsyncResult.AsyncResult<A, E>): Option.Option<number> => {
if (result._tag === "Success") {
return Option.some(result.timestamp)
}
if (result._tag === "Failure") {
return Option.map(result.previousSuccess, (success) => success.timestamp)
}
return Option.none()
}
const isFreshWithin = (timestamp: number, staleTime: number, now: number): boolean => now - timestamp < staleTime
const shouldRevalidateSWR = <A, E>(result: AsyncResult.AsyncResult<A, E>, staleTime: number): boolean => {
if (result.waiting) {
return false
}
const timestamp = Option.getOrUndefined(swrTimestamp(result))
if (timestamp === undefined) {
return result._tag !== "Initial"
}
return !isFreshWithin(timestamp, staleTime, Date.now())
}
/**
* Wraps an atom in a writable optimistic atom.
*
* **Details**
*
* Writes accept transition atoms containing `AsyncResult` values. Waiting
* successes are shown optimistically while transitions run; when successful
* transitions finish, the source atom is refreshed, and failures roll the value
* back to the latest source value.
*
* @category Optimistic
* @since 4.0.0
*/
export const optimistic = <A>(self: Atom<A>): Writable<A, Atom<AsyncResult.AsyncResult<A, unknown>>> => {
let counter = 0
const writeAtom = removeTtl(state(
[
counter,
undefined as any as Atom<AsyncResult.AsyncResult<A, unknown>>
] as const
))
return writable(
(get) => {
let lastValue = get.once(self)
let needsRefresh = false
get.subscribe(self, (value) => {
lastValue = value
if (transitions.size > 0) {
return
}
needsRefresh = false
if (!AsyncResult.isAsyncResult(value)) {
return get.setSelf(value)
}
const current = Option.getOrUndefined(get.self<AsyncResult.AsyncResult<any, any>>())!
switch (value._tag) {
case "Initial": {
if (AsyncResult.isInitial(current)) {
get.setSelf(value)
}
return
}
case "Success": {
if (AsyncResult.isSuccess(current)) {
if (!value.waiting && value.timestamp >= current.timestamp) {
get.setSelf(value)
}
} else {
get.setSelf(value)
}
return
}
case "Failure": {
return get.setSelf(value)
}
}
})
const transitions = new Set<Atom<AsyncResult.AsyncResult<A, unknown>>>()
const cancels = new Set<() => void>()
get.subscribe(writeAtom, ([, atom]) => {
if (transitions.has(atom)) return
transitions.add(atom)
let cancel: (() => void) | undefined
// eslint-disable-next-line prefer-const
cancel = get.registry.subscribe(atom, (result) => {
if (AsyncResult.isSuccess(result) && result.waiting) {
return get.setSelf(result.value)
}
transitions.delete(atom)
if (cancel) {
cancels.delete(cancel)
cancel()
}
if (!needsRefresh && !AsyncResult.isFailure(result)) {
needsRefresh = true
}
if (transitions.size === 0) {
if (needsRefresh) {
needsRefresh = false
get.refresh(self)
} else {
get.setSelf(lastValue)
}
}
}, { immediate: true })
if (transitions.has(atom)) {
cancels.add(cancel)
} else {
cancel()
}
})
get.addFinalizer(() => {
for (const cancel of cancels) cancel()
transitions.clear()
cancels.clear()
})
return lastValue
},
(ctx, atom) => ctx.set(writeAtom, [++counter, atom]),
(refresh) => refresh(self)
)
}
/**
* Creates an `AtomResultFn` that applies an optimistic update before running the
* underlying mutation.
*
* **Details**
*
* The reducer computes the provisional value from the current value and mutation
* input. The wrapped function result then completes the transition or updates the
* optimistic value through the provided setter callback.
*
* @category Optimistic
* @since 4.0.0
*/
export const optimisticFn: {
/**
* Creates an `AtomResultFn` that applies an optimistic update before running the
* underlying mutation.
*
* **Details**
*
* The reducer computes the provisional value from the current value and mutation
* input. The wrapped function result then completes the transition or updates the
* optimistic value through the provided setter callback.
*
* @category Optimistic
* @since 4.0.0
*/
<A, W, XA, XE, OW = void>(
options: {
readonly reducer: (current: NoInfer<A>, update: OW) => NoInfer<W>
readonly fn:
| AtomResultFn<OW, XA, XE>
| ((set: (result: NoInfer<W>) => void) => AtomResultFn<OW, XA, XE>)
}
): (
self: Writable<A, Atom<AsyncResult.AsyncResult<W, unknown>>>
) => AtomResultFn<OW, XA, XE>
/**
* Creates an `AtomResultFn` that applies an optimistic update before running the
* underlying mutation.
*
* **Details**
*
* The reducer computes the provisional value from the current value and mutation
* input. The wrapped function result then completes the transition or updates the
* optimistic value through the provided setter callback.
*
* @category Optimistic
* @since 4.0.0
*/
<A, W, XA, XE, OW = void>(
self: Writable<A, Atom<AsyncResult.AsyncResult<W, unknown>>>,
options: {
readonly reducer: (current: NoInfer<A>, update: OW) => NoInfer<W>
readonly fn:
| AtomResultFn<OW, XA, XE>
| ((set: (result: NoInfer<W>) => void) => AtomResultFn<OW, XA, XE>)
}
): AtomResultFn<OW, XA, XE>
} = dual(2, <A, W, XA, XE, OW = void>(
self: Writable<A, Atom<AsyncResult.AsyncResult<W, unknown>>>,
options: {
readonly reducer: (current: NoInfer<A>, update: OW) => NoInfer<W>
readonly fn:
| AtomResultFn<OW, XA, XE>
| ((set: (result: NoInfer<W>) => void) => AtomResultFn<OW, XA, XE>)
}
): AtomResultFn<OW, XA, XE> => {
const transition = removeTtl(state</**
* Creates an `AtomResultFn` that applies an optimistic update before running the
* underlying mutation.
*
* **Details**
*
* The reducer computes the provisional value from the current value and mutation
* input. The wrapped function result then completes the transition or updates the
* optimistic value through the provided setter callback.
*
* @category Optimistic
* @since 4.0.0
*/
AsyncResult.AsyncResult<W, unknown>>(AsyncResult.initial()))
return fn((arg: OW, get) => {
let value = options.reducer(get(self), arg)
if (AsyncResult.isAsyncResult(value)) {
value = AsyncResult.waiting(value, { touch: true })
}
get.set(transition, AsyncResult.success(value, { waiting: true }))
get.set(self, transition)
const fn = typeof options.fn === "function"
? autoDispose(options.fn((value) =>
get.set(
transition,
AsyncResult.success(AsyncResult.isAsyncResult(value) ? AsyncResult.waiting(value) : value, { waiting: true })
)
))
: options.fn
get.set(fn, arg)
return Effect.callback</**
* Creates an `AtomResultFn` that applies an optimistic update before running the
* underlying mutation.
*
* **Details**
*
* The reducer computes the provisional value from the current value and mutation
* input. The wrapped function result then completes the transition or updates the
* optimistic value through the provided setter callback.
*
* @category Optimistic
* @since 4.0.0
*/
XA, /**
* Creates an `AtomResultFn` that applies an optimistic update before running the
* underlying mutation.
*
* **Details**
*
* The reducer computes the provisional value from the current value and mutation
* input. The wrapped function result then completes the transition or updates the
* optimistic value through the provided setter callback.
*
* @category Optimistic
* @since 4.0.0
*/
XE>((resume) => {
get.subscribe(fn, (result) => {
if (result._tag === "Initial" || result.waiting) return
get.set(transition, AsyncResult.map(result, () => value))
resume(AsyncResult.toExit(result) as any)
}, { immediate: true })
});
});
})
/**
* Runs synchronous atom updates as a batch.
*
* **Details**
*
* Stale nodes are rebuilt and listeners are notified after the callback completes,
* so dependent updates observe the final batched state.
*
* @category batching
* @since 4.0.0
*/
export const batch: (f: () => void) => void = Registry.batch
// -----------------------------------------------------------------------------
// Focus
// -----------------------------------------------------------------------------
/**
* Creates a browser-only signal atom that increments when the document becomes visible.
*
* **Details**
*
* It listens for `visibilitychange` events on `window` and removes the listener
* when the atom is disposed.
*
* @category Focus
* @since 4.0.0
*/
export const windowFocusSignal: Atom<number> = readable((get) => {
let count = 0
function update() {
if (document.visibilityState === "visible") {
get.setSelf(++count)
}
}
window.addEventListener("visibilitychange", update)
get.addFinalizer(() => {
window.removeEventListener("visibilitychange", update)
})
return count
})
/**
* Creates a combinator that refreshes an atom whenever the supplied signal atom
* changes.
*
* **Details**
*
* The derived atom also subscribes to the source atom so normal source updates are
* forwarded to its own value.
*
* @category Focus
* @since 4.0.0
*/
export const makeRefreshOnSignal = <_>(signal: Atom<_>) => <A extends Atom<any>>(self: A): WithoutSerializable<A> =>
transform(self, (get) => {
get.once(signal)
get.subscribe(signal, (_) => get.refresh(self))
get.subscribe(self, (value) => get.setSelf(value))
return get.once(self)
}, { initialValueTarget: self }) as any
/**
* Refreshes an atom whenever `windowFocusSignal` changes.
*
* **Details**
*
* This helper is browser-only because `windowFocusSignal` depends on `window` and
* `document.visibilityState`.
*
* @category Focus
* @since 4.0.0
*/
export const refreshOnWindowFocus: <A extends Atom<any>>(self: A) => WithoutSerializable<A> = makeRefreshOnSignal(
windowFocusSignal
)
// -----------------------------------------------------------------------------
// KeyValueStore
// -----------------------------------------------------------------------------
/**
* Creates a writable atom backed by a `KeyValueStore` entry.
*
* **Details**
*
* Values are encoded and decoded with the supplied schema. In sync mode the atom
* exposes the decoded value and writes the default value when the key is missing;
* in async mode it exposes an `AsyncResult` of the decoded value.
*
* @category KeyValueStore
* @since 4.0.0
*/
export const kvs = <S extends Schema.Codec<any, any>, const Mode extends "sync" | "async" = never>(options: {
readonly runtime: AtomRuntime<KeyValueStore.KeyValueStore, any>
readonly key: string
readonly schema: S
readonly defaultValue: LazyArg<S["Type"]>
readonly mode?: Mode | undefined
}): Writable<"async" extends Mode ? AsyncResult.AsyncResult<S["Type"]> : S["Type"], S["Type"]> => {
const setAtom = options.runtime.fn(
(value: S["Type"]) =>
KeyValueStore.KeyValueStore.use((store) =>
KeyValueStore.toSchemaStore(store, options.schema).set(options.key, value)
)
)
const resultAtom = options.runtime.atom(
KeyValueStore.KeyValueStore.use((store) => KeyValueStore.toSchemaStore(store, options.schema).get(options.key))
)
return writable(
options.mode === "async" ?
(get) => {
get.mount(setAtom)
const mapper = AsyncResult.map<Option.Option<S["Type"]>, S["Type"]>(
Option.getOrElse(() => {
const value = options.defaultValue()
get.set(setAtom, value)
return value
})
)
get.subscribe(resultAtom, (result) => get.setSelf(mapper(result)))
return mapper(get.once(resultAtom))
} :
(get) => {
get.mount(setAtom)
get.subscribe(resultAtom, (result) => {
if (!AsyncResult.isSuccess(result)) return
if (Option.isSome(result.value)) {
get.setSelf(result.value.value)
} else {
const value = Option.getOrElse(get.self<S["Type"]>(), options.defaultValue)
get.setSelf(value)
get.set(setAtom, value)
}
}, { immediate: true })
return Option.getOrElse(get.self<S["Type"]>(), options.defaultValue)
},
(ctx, value: S["Type"]) => {
ctx.set(setAtom, value as any)
ctx.setSelf(value)
}
) as any
}
// -----------------------------------------------------------------------------
// URL search params
// -----------------------------------------------------------------------------
/**
* Creates an atom that reads and writes a URL search parameter.
*
* **Gotchas**
*
* If you pass a schema, it has to be synchronous and have no context.
*
* @category search params
* @since 4.0.0
*/
export const searchParam = <S extends Schema.Codec<any, string> = never>(name: string, options?: {
readonly schema?: S | undefined
}): Writable<[S] extends [never] ? string : Option.Option<S["Type"]>> => {
const decode = options?.schema && Schema.decodeExit(options.schema)
const encode = options?.schema && Schema.encodeExit(options.schema)
return writable(
(get) => {
if (typeof window === "undefined") {
return decode ? Option.none() : ""
}
const handleUpdate = () => {
if (searchParamState.updating) return
const searchParams = new URLSearchParams(window.location.search)
const newValue = searchParams.get(name) || ""
if (decode) {
get.setSelf(Exit.getSuccess(decode(newValue)))
} else if (newValue !== Option.getOrUndefined(get.self())) {
get.setSelf(newValue)
}
}
window.addEventListener("popstate", handleUpdate)
window.addEventListener("pushstate", handleUpdate)
get.addFinalizer(() => {
window.removeEventListener("popstate", handleUpdate)
window.removeEventListener("pushstate", handleUpdate)
})
const value = new URLSearchParams(window.location.search).get(name) || ""
return decode ? Exit.getSuccess(decode(value)) : value as any
},
(ctx, value: any) => {
if (typeof window === "undefined") {
ctx.setSelf(value)
return
}
if (encode) {
const encoded = Option.flatMap(value, (v) => Exit.getSuccess(encode(v as S["Type"])))
searchParamState.updates.set(name, Option.getOrElse(encoded, () => ""))
value = Option.zipRight(encoded, value)
} else {
searchParamState.updates.set(name, value)
}
ctx.setSelf(value)
if (searchParamState.timeout) {
clearTimeout(searchParamState.timeout)
}
searchParamState.timeout = setTimeout(updateSearchParams, 500) as any
}
)
}
const searchParamState = {
timeout: undefined as number | undefined,
updates: new Map<string, string>(),
updating: false
}
function updateSearchParams() {
searchParamState.timeout = undefined
searchParamState.updating = true
const searchParams = new URLSearchParams(window.location.search)
for (const [key, value] of searchParamState.updates.entries()) {
if (value.length > 0) {
searchParams.set(key, value)
} else {
searchParams.delete(key)
}
}
searchParamState.updates.clear()
const newUrl = `${window.location.pathname}?${searchParams.toString()}`
window.history.pushState({}, "", newUrl)
searchParamState.updating = false
}
// -----------------------------------------------------------------------------
// conversions
// -----------------------------------------------------------------------------
/**
* Converts an atom into a stream using the `AtomRegistry` service.
*
* **Details**
*
* The stream emits the atom's current value immediately and then emits subsequent
* changes until the stream scope is closed.
*
* @category converting
* @since 4.0.0
*/
export const toStream = <A>(self: Atom<A>): Stream.Stream<A, never, AtomRegistry> =>
Stream.unwrap(AtomRegistry.use((r) => Effect.succeed(Registry.toStream(r, self))))
/**
* Converts an `AsyncResult` atom into a stream using the `AtomRegistry` service.
*
* **Details**
*
* Initial results are skipped, successes are emitted as stream values, and
* failures fail the stream with the result cause.
*
* @category converting
* @since 4.0.0
*/
export const toStreamResult = <A, E>(self: Atom<AsyncResult.AsyncResult<A, E>>): Stream.Stream<A, E, AtomRegistry> =>
Stream.unwrap(AtomRegistry.use((r) => Effect.succeed(Registry.toStreamResult(r, self))))
/**
* Reads an atom's current value from the `AtomRegistry` service.
*
* @category converting
* @since 4.0.0
*/
export const get = <A>(self: Atom<A>): Effect.Effect<A, never, AtomRegistry> =>
AtomRegistry.use((r) => Effect.succeed(r.get(self)))
/**
* Reads a writable atom, computes a return value and next write value, writes the
* next value, and returns the computed result.
*
* @category converting
* @since 4.0.0
*/
export const modify: {
/**
* Reads a writable atom, computes a return value and next write value, writes the
* next value, and returns the computed result.
*
* @category converting
* @since 4.0.0
*/
<R, W, A>(f: (_: R) => [returnValue: A, nextValue: W]): (self: Writable<R, W>) => Effect.Effect<A, never, AtomRegistry>
/**
* Reads a writable atom, computes a return value and next write value, writes the
* next value, and returns the computed result.
*
* @category converting
* @since 4.0.0
*/
<R, W, A>(self: Writable<R, W>, f: (_: R) => [returnValue: A, nextValue: W]): Effect.Effect<A, never, AtomRegistry>
} = dual(
2,
<R, W, A>(self: Writable<R, W>, f: (_: R) => [returnValue: A, nextValue: W]): Effect.Effect<A, never, AtomRegistry> =>
Effect.map(AtomRegistry, (_) => _.modify(self, f))
)
/**
* Writes a value to a writable atom through the `AtomRegistry` service.
*
* @category converting
* @since 4.0.0
*/
export const set: {
/**
* Writes a value to a writable atom through the `AtomRegistry` service.
*
* @category converting
* @since 4.0.0
*/
<W>(value: W): <R>(self: Writable<R, W>) => Effect.Effect<void, never, AtomRegistry>
/**
* Writes a value to a writable atom through the `AtomRegistry` service.
*
* @category converting
* @since 4.0.0
*/
<R, W>(self: Writable<R, W>, value: W): Effect.Effect<void, never, AtomRegistry>
} = dual(
2,
<R, W>(self: Writable<R, W>, value: W): Effect.Effect<void, never, AtomRegistry> =>
Effect.map(AtomRegistry, (_) => _.set(self, value))
)
/**
* Updates a writable atom by reading its current value from the registry and
* writing the value returned by the update function.
*
* @category converting
* @since 4.0.0
*/
export const update: {
/**
* Updates a writable atom by reading its current value from the registry and
* writing the value returned by the update function.
*
* @category converting
* @since 4.0.0
*/
<R, W>(f: (_: R) => W): (self: Writable<R, W>) => Effect.Effect<void, never, AtomRegistry>
/**
* Updates a writable atom by reading its current value from the registry and
* writing the value returned by the update function.
*
* @category converting
* @since 4.0.0
*/
<R, W>(self: Writable<R, W>, f: (_: R) => W): Effect.Effect<void, never, AtomRegistry>
} = dual(
2,
<R, W>(self: Writable<R, W>, f: (_: R) => W): Effect.Effect<void, never, AtomRegistry> =>
Effect.map(AtomRegistry, (_) => _.update(self, f))
)
/**
* Reads an `AsyncResult` atom as an effect through the `AtomRegistry` service.
*
* **Details**
*
* The effect waits while the result is `Initial`, and also while it is waiting
* when `suspendOnWaiting` is enabled. Successes succeed with the value and
* failures fail with the result cause.
*
* @category converting
* @since 4.0.0
*/
export const getResult = <A, E>(
self: Atom<AsyncResult.AsyncResult<A, E>>,
options?: { readonly suspendOnWaiting?: boolean | undefined }
): Effect.Effect<A, E, AtomRegistry> => AtomRegistry.use(Registry.getResult(self, options))
/**
* Runs a refresh request for an atom through the `AtomRegistry` service.
*
* **When to use**
*
* Use to invalidate and recompute an atom from an Effect that has access to the
* active registry.
*
* @category converting
* @since 4.0.0
*/
export const refresh = <A>(self: Atom<A>): Effect.Effect<void, never, AtomRegistry> =>
Effect.map(AtomRegistry, (_) => _.refresh(self))
/**
* Mounts an atom in the `AtomRegistry` for the lifetime of the current scope.
*
* **Details**
*
* Mounting keeps the atom subscribed with a no-op listener until the scope
* finalizer releases it.
*
* @category converting
* @since 4.0.0
*/
export const mount = <A>(self: Atom<A>): Effect.Effect<void, never, AtomRegistry | Scope.Scope> =>
AtomRegistry.use((r) => Registry.mount(r, self))
// -----------------------------------------------------------------------------
// Serializable
// -----------------------------------------------------------------------------
/**
* The type id used to mark atoms that carry serialization metadata.
*
* @category type IDs
* @since 4.0.0
*/
export const SerializableTypeId: SerializableTypeId = "~effect-atom/atom/Atom/Serializable"
/**
* The literal type of the serializable atom marker.
*
* @category type IDs
* @since 4.0.0
*/
export type SerializableTypeId = "~effect-atom/atom/Atom/Serializable"
/**
* Serialization metadata attached to an atom.
*
* **Details**
*
* The key identifies the atom in dehydrated state, and the encode/decode
* functions convert between the atom value and the schema encoded value.
*
* @category Serializable
* @since 4.0.0
*/
export interface Serializable<S extends Schema.Top> {
readonly [SerializableTypeId]: {
readonly key: string
readonly encode: (value: S["Type"]) => S["Encoded"]
readonly decode: (value: S["Encoded"]) => S["Type"]
}
}
/**
* Returns `true` when an atom carries `Serializable` metadata.
*
* @category Serializable
* @since 4.0.0
*/
export const isSerializable = (self: Atom<any>): self is Atom<any> & Serializable<any> => SerializableTypeId in self
/**
* Attaches serialization metadata to an atom using a schema and stable key.
*
* **Details**
*
* The schema is converted to a JSON codec for synchronous encode/decode, and the
* key is also used as the atom label when the atom does not already have one.
*
* @category combinators
* @since 4.0.0
*/
export const serializable: {
/**
* Attaches serialization metadata to an atom using a schema and stable key.
*
* **Details**
*
* The schema is converted to a JSON codec for synchronous encode/decode, and the
* key is also used as the atom label when the atom does not already have one.
*
* @category combinators
* @since 4.0.0
*/
<R extends Atom<any>, S extends Schema.Codec<Type<R>, any>>(
options: {
readonly key: string
readonly schema: S
}
): (self: R) => R & Serializable<S>
/**
* Attaches serialization metadata to an atom using a schema and stable key.
*
* **Details**
*
* The schema is converted to a JSON codec for synchronous encode/decode, and the
* key is also used as the atom label when the atom does not already have one.
*
* @category combinators
* @since 4.0.0
*/
<R extends Atom<any>, S extends Schema.Codec<Type<R>, any>>(
self: R,
options: {
readonly key: string
readonly schema: S
}
): R & Serializable<S>
} = dual(2, <R extends Atom<any>, A, I>(self: R, options: {
readonly key: string
readonly schema: Schema.Codec<A, I>
}): R & Serializable<any> => {
const codecJson = Schema.toCodecJson(options.schema)
return Object.assign(Object.create(Object.getPrototypeOf(self)), {
...self,
label: self.label ?? [options.key, new Error().stack?.split("\n")[5] ?? ""],
[SerializableTypeId]: {
key: options.key,
encode: Schema.encodeSync(codecJson),
decode: Schema.decodeSync(codecJson)
}
})
})
/**
* The type id used to mark atoms with a server-side read override.
*
* @category type IDs
* @since 4.0.0
*/
export const ServerValueTypeId = "~effect-atom/atom/Atom/ServerValue" as const
/**
* Sets the value of an Atom when read on the server.
*
* @category ServerValue
* @since 4.0.0
*/
export const withServerValue: {
/**
* Sets the value of an Atom when read on the server.
*
* @category ServerValue
* @since 4.0.0
*/
<A extends Atom<any>>(read: (get: <A>(atom: Atom<A>) => A) => Type<A>): (self: A) => A
/**
* Sets the value of an Atom when read on the server.
*
* @category ServerValue
* @since 4.0.0
*/
<A extends Atom<any>>(self: A, read: (get: <A>(atom: Atom<A>) => A) => Type<A>): A
} = dual(
2,
<A extends Atom<any>>(self: A, read: (get: <A>(atom: Atom<A>) => A) => Type<A>): A =>
Object.assign(Object.create(Object.getPrototypeOf(self)), {
...self,
[ServerValueTypeId]: read
})
)
/**
* Sets an `AsyncResult` atom's server-side value to
* `AsyncResult.initial(true)`.
*
* @category ServerValue
* @since 4.0.0
*/
export const withServerValueInitial = <A extends Atom<AsyncResult.AsyncResult<any, any>>>(self: A): A =>
withServerValue(self, constant(AsyncResult.initial(true)) as any)
/**
* Reads an atom from a registry, using its server-side read override when one is
* present.
*
* **Details**
*
* Nested reads performed by the override are resolved against the same registry.
*
* @category ServerValue
* @since 4.0.0
*/
export const getServerValue: {
/**
* Reads an atom from a registry, using its server-side read override when one is
* present.
*
* **Details**
*
* Nested reads performed by the override are resolved against the same registry.
*
* @category ServerValue
* @since 4.0.0
*/
(registry: Registry.AtomRegistry): <A>(self: Atom<A>) => A
/**
* Reads an atom from a registry, using its server-side read override when one is
* present.
*
* **Details**
*
* Nested reads performed by the override are resolved against the same registry.
*
* @category ServerValue
* @since 4.0.0
*/
<A>(self: Atom<A>, registry: Registry.AtomRegistry): A
} = dual(
2,
<A>(self: Atom<A>, registry: Registry.AtomRegistry): A =>
ServerValueTypeId in self
? (self as any)[ServerValueTypeId]((atom: Atom<any>) => registry.get(atom))
: registry.get(self)
)

Xet Storage Details

Size:
103 kB
·
Xet hash:
870d22e8014f9fad78e3246776acdc1bd3b228b81f48a19f34aa67d9da9e621a

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.