Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 017cb80149 | |||
| 3fc3904c6c | |||
| 23cdfb3894 | |||
| 7a7783e7de | |||
| e07ce1cd68 | |||
| c59e2ee035 | |||
| e5ac120cea | |||
| 015aa0c816 | |||
| 5945953555 | |||
| 65fcfb395e | |||
| a86cb7e024 | |||
| 24ce543092 | |||
| 6b5c501a98 |
@@ -58,16 +58,18 @@
|
|||||||
"name": "effect-fc-next",
|
"name": "effect-fc-next",
|
||||||
"version": "0.1.0-beta.0",
|
"version": "0.1.0-beta.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect-lens": "2.0.0-beta.0",
|
"effect-lens": "^2.0.0-beta.1",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/platform-browser": "4.0.0-beta.98",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
|
"effect": "4.0.0-beta.98",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"effect": "4.0.0-beta.85",
|
"effect": "4.0.0-beta.98",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -3062,9 +3064,11 @@
|
|||||||
|
|
||||||
"dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
|
"dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
|
||||||
|
|
||||||
"effect-fc-next/effect": ["effect@4.0.0-beta.85", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Cjv9YQyv4CiIccmRAQIWAoeESCpCpiuHYY8zb5vqiYs3Ac2yE5RQAnKq7z4Ir/3VJYZb8kxh6rS8czDDnpTdkQ=="],
|
"effect-fc-next/@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.98", "", { "dependencies": { "multipasta": "^0.2.8" }, "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-Y0BEe0N8clKmJTHqmZpS85pLZbqAxP5uV3IYF0tDw/1dcs8AbTRHjbRmOLdyLtmU32NBVAiaUD3Vopb5Et5R/g=="],
|
||||||
|
|
||||||
"effect-fc-next/effect-lens": ["effect-lens@2.0.0-beta.0", "", { "peerDependencies": { "effect": "4.0.0-beta.85" } }, "sha512-ntsRQSrzoX+GsNpkNVEGqebl/g1YOEDPIFRAHigf+4P6AtGnnOFcklqDHLrbm4zqnaxuWEgQvvn1J6S3eyC4yw=="],
|
"effect-fc-next/effect": ["effect@4.0.0-beta.98", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-oz+bsG5h+6RNrw4t5GMfQrk/xBS8ROoqkYsuvRhBr5O7mCOrpvH/hbw+QrDzvKIpX4HJClwm86F94c87W0sJxg=="],
|
||||||
|
|
||||||
|
"effect-fc-next/effect-lens": ["effect-lens@2.0.0-beta.1", "", { "peerDependencies": { "effect": "4.0.0-beta.98" } }, "sha512-3MwsrezPczZHH4Tl2SvliZdiNRq9e/XbH+Vr1ckr/FPCEp6Wv8IYQL4Ie/hhI+XBiLlxbQERbgL8FlHW21avtw=="],
|
||||||
|
|
||||||
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||||
|
|
||||||
@@ -3370,10 +3374,16 @@
|
|||||||
|
|
||||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||||
|
|
||||||
"effect-fc-next/effect/fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="],
|
"effect-fc-next/@effect/platform-browser/multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="],
|
||||||
|
|
||||||
|
"effect-fc-next/effect/fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="],
|
||||||
|
|
||||||
"effect-fc-next/effect/msgpackr": ["msgpackr@2.0.4", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA=="],
|
"effect-fc-next/effect/msgpackr": ["msgpackr@2.0.4", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA=="],
|
||||||
|
|
||||||
|
"effect-fc-next/effect/multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="],
|
||||||
|
|
||||||
|
"effect-fc-next/effect/toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="],
|
||||||
|
|
||||||
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"file-loader/schema-utils/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
"file-loader/schema-utils/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||||
|
|||||||
@@ -39,16 +39,18 @@
|
|||||||
"clean:modules": "rm -rf node_modules"
|
"clean:modules": "rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@effect/platform-browser": "4.0.0-beta.98",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
|
"effect": "4.0.0-beta.98",
|
||||||
"jsdom": "^29.0.0",
|
"jsdom": "^29.0.0",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"effect": "4.0.0-beta.85",
|
"effect": "4.0.0-beta.98",
|
||||||
"react": "^19.2.0"
|
"react": "^19.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect-lens": "2.0.0-beta.0"
|
"effect-lens": "^2.0.0-beta.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||||
import { Context, Effect, type Equivalence, Function, Predicate, Scope } from "effect"
|
import { type Context, Effect, type Equivalence, Function, Predicate, Scope } from "effect"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as Component from "./Component.js"
|
import * as Component from "./Component.js"
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ export const AsyncPrototype: AsyncPrototype = Object.freeze({
|
|||||||
|
|
||||||
return ({ fallback, name, ...props }: AsyncProps) => {
|
return ({ fallback, name, ...props }: AsyncProps) => {
|
||||||
const promise = Effect.runPromiseWith(contextRef.current)(
|
const promise = Effect.runPromiseWith(contextRef.current)(
|
||||||
Effect.andThen(
|
Effect.flatMap(
|
||||||
Component.useScope([], this),
|
Component.useScope([], this),
|
||||||
scope => Effect.provideService(this.body(props as P), Scope.Scope, scope),
|
scope => Effect.provideService(this.body(props as P), Scope.Scope, scope),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,385 +0,0 @@
|
|||||||
/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */
|
|
||||||
import {
|
|
||||||
type Context,
|
|
||||||
type Duration,
|
|
||||||
Effect,
|
|
||||||
Exit,
|
|
||||||
Function,
|
|
||||||
identity,
|
|
||||||
Layer,
|
|
||||||
Pipeable,
|
|
||||||
Predicate,
|
|
||||||
Scope,
|
|
||||||
Tracer,
|
|
||||||
} from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export const ComponentTypeId: unique symbol = Symbol.for("@effect-fc/Component/Component")
|
|
||||||
export type ComponentTypeId = typeof ComponentTypeId
|
|
||||||
|
|
||||||
export interface Component<P extends {}, A extends React.ReactNode, E, R, F extends Component.Signature>
|
|
||||||
extends ComponentPrototype<R, F>, ComponentOptions {
|
|
||||||
new(_: never): Record<string, never>
|
|
||||||
readonly [ComponentTypeId]: ComponentTypeId
|
|
||||||
readonly "~Props": P
|
|
||||||
readonly "~Success": A
|
|
||||||
readonly "~Error": E
|
|
||||||
readonly "~Context": R
|
|
||||||
readonly "~Function": F
|
|
||||||
readonly body: (props: P) => Effect.Effect<A, E, R>
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare namespace Component {
|
|
||||||
export type Default<P extends {}, A extends React.ReactNode, E, R> = Component<P, A, E, R, DefaultSignature<P, A>>
|
|
||||||
export type Any = Component<any, any, any, any, any>
|
|
||||||
export type Signature = (props: any) => React.ReactNode
|
|
||||||
export type DefaultSignature<P extends {}, A extends React.ReactNode> = (props: P) => A
|
|
||||||
export type Props<T extends Any> = T["~Props"]
|
|
||||||
export type Success<T extends Any> = T["~Success"]
|
|
||||||
export type Error<T extends Any> = T["~Error"]
|
|
||||||
export type Context<T extends Any> = T["~Context"]
|
|
||||||
export type Function<T extends Any> = T["~Function"]
|
|
||||||
export type AsComponent<T extends Any> = Component<Props<T>, Success<T>, Error<T>, Context<T>, Function<T>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComponentOptions {
|
|
||||||
readonly displayName?: string
|
|
||||||
readonly nonReactiveTags: readonly Context.Key<any, any>[]
|
|
||||||
readonly finalizerExecutionStrategy: "sequential" | "parallel"
|
|
||||||
readonly finalizerExecutionDebounce: Duration.Input
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultOptions: ComponentOptions = {
|
|
||||||
nonReactiveTags: [Tracer.ParentSpan],
|
|
||||||
finalizerExecutionStrategy: "sequential",
|
|
||||||
finalizerExecutionDebounce: "100 millis",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComponentPrototype<R, F extends Component.Signature> extends Pipeable.Pipeable {
|
|
||||||
readonly [ComponentTypeId]: ComponentTypeId
|
|
||||||
readonly use: Effect.Effect<F, never, Exclude<R, Scope.Scope>>
|
|
||||||
}
|
|
||||||
|
|
||||||
type ComponentImpl = Component<any, React.ReactNode, any, any, Component.Signature>
|
|
||||||
|
|
||||||
const makeFunctionComponent = (
|
|
||||||
self: ComponentImpl,
|
|
||||||
contextRef: React.RefObject<Context.Context<any>>,
|
|
||||||
): Component.Signature => {
|
|
||||||
if ("asFunctionComponent" in self && typeof self.asFunctionComponent === "function") {
|
|
||||||
return self.asFunctionComponent(contextRef)
|
|
||||||
}
|
|
||||||
const FunctionComponent = (props: {}) => Effect.runSyncWith(contextRef.current)(
|
|
||||||
Effect.flatMap(
|
|
||||||
useScope([], self),
|
|
||||||
scope => Effect.provideService(self.body(props), Scope.Scope, scope),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
FunctionComponent.displayName = self.displayName ?? "Anonymous"
|
|
||||||
return "transformFunctionComponent" in self && typeof self.transformFunctionComponent === "function"
|
|
||||||
? self.transformFunctionComponent(FunctionComponent)
|
|
||||||
: FunctionComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
const use = Effect.fnUntraced(function* (self: ComponentImpl) {
|
|
||||||
const context = yield* Effect.context<any>()
|
|
||||||
const cached = componentCache.get(self)
|
|
||||||
if (cached !== undefined) {
|
|
||||||
cached.contextRef.current = context
|
|
||||||
return cached.component
|
|
||||||
}
|
|
||||||
const contextRef = { current: context }
|
|
||||||
const component = makeFunctionComponent(self, contextRef)
|
|
||||||
componentCache.set(self, { contextRef, component })
|
|
||||||
return component
|
|
||||||
})
|
|
||||||
|
|
||||||
const componentCache = new WeakMap<ComponentImpl, {
|
|
||||||
readonly contextRef: { current: Context.Context<any> }
|
|
||||||
readonly component: Component.Signature
|
|
||||||
}>()
|
|
||||||
|
|
||||||
export const ComponentPrototype = Object.freeze({
|
|
||||||
[ComponentTypeId]: ComponentTypeId,
|
|
||||||
...Pipeable.Prototype,
|
|
||||||
get use() {
|
|
||||||
return use(this as ComponentImpl)
|
|
||||||
},
|
|
||||||
}) as unknown as ComponentPrototype<any, any>
|
|
||||||
|
|
||||||
export const isComponent = (u: unknown): u is Component.Any => Predicate.hasProperty(u, ComponentTypeId)
|
|
||||||
|
|
||||||
type GeneratorBody<P extends {}, A extends React.ReactNode, E, R> = (
|
|
||||||
props: P,
|
|
||||||
) => Effect.fn.Return<A, E, R>
|
|
||||||
|
|
||||||
type EffectBody<P extends {}, A extends React.ReactNode, E, R> = (
|
|
||||||
props: P,
|
|
||||||
) => Effect.Effect<A, E, R>
|
|
||||||
|
|
||||||
export interface Make {
|
|
||||||
<P extends {}, A extends React.ReactNode, E = never, R = never>(
|
|
||||||
body: GeneratorBody<P, A, E, R> | EffectBody<P, A, E, R>,
|
|
||||||
...pipeables: readonly Function[]
|
|
||||||
): Component.Default<P, A, E, R>
|
|
||||||
(name: string, options?: Tracer.SpanOptionsNoTrace): <P extends {}, A extends React.ReactNode, E = never, R = never>(
|
|
||||||
body: GeneratorBody<P, A, E, R> | EffectBody<P, A, E, R>,
|
|
||||||
...pipeables: readonly Function[]
|
|
||||||
) => Component.Default<P, A, E, R>
|
|
||||||
}
|
|
||||||
|
|
||||||
const component = (
|
|
||||||
body: Function,
|
|
||||||
displayName: string | undefined,
|
|
||||||
traced: boolean,
|
|
||||||
pipeables: readonly Function[],
|
|
||||||
): Component.Any => Object.setPrototypeOf(
|
|
||||||
Object.assign(() => {}, defaultOptions, {
|
|
||||||
body: traced && displayName
|
|
||||||
? Effect.fn(displayName)(body as never, ...pipeables as [])
|
|
||||||
: Effect.fnUntraced(body as never, ...pipeables as []),
|
|
||||||
displayName,
|
|
||||||
}),
|
|
||||||
ComponentPrototype,
|
|
||||||
)
|
|
||||||
|
|
||||||
export const make: Make = ((nameOrBody: string | Function, ...args: readonly unknown[]) => {
|
|
||||||
if (typeof nameOrBody === "string") {
|
|
||||||
return (body: Function, ...pipeables: readonly Function[]) => component(body, nameOrBody, true, pipeables)
|
|
||||||
}
|
|
||||||
return component(nameOrBody, undefined, true, args as readonly Function[])
|
|
||||||
}) as Make
|
|
||||||
|
|
||||||
export const makeUntraced: Make = ((nameOrBody: string | Function, ...args: readonly unknown[]) => {
|
|
||||||
if (typeof nameOrBody === "string") {
|
|
||||||
return (body: Function, ...pipeables: readonly Function[]) => component(body, nameOrBody, false, pipeables)
|
|
||||||
}
|
|
||||||
return component(nameOrBody, undefined, false, args as readonly Function[])
|
|
||||||
}) as Make
|
|
||||||
|
|
||||||
export declare namespace withSignature {
|
|
||||||
export type Result<T extends Component.Any, F extends Component.Signature> = (
|
|
||||||
& Omit<T, keyof Component.AsComponent<T>>
|
|
||||||
& Component<Component.Props<T>, Component.Success<T>, Component.Error<T>, Component.Context<T>, F>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const withSignature: {
|
|
||||||
<F extends Component.Signature>(): <T extends Component.Any>(self: T) => withSignature.Result<T, F>
|
|
||||||
<F extends Component.Signature, T extends Component.Any>(self: T): withSignature.Result<T, F>
|
|
||||||
} = (self?: Component.Any): any => self === undefined ? identity : self
|
|
||||||
|
|
||||||
export const withOptions: {
|
|
||||||
<T extends Component.Any>(options: Partial<ComponentOptions>): (self: T) => T
|
|
||||||
<T extends Component.Any>(self: T, options: Partial<ComponentOptions>): T
|
|
||||||
} = Function.dual(2, <T extends Component.Any>(self: T, options: Partial<ComponentOptions>): T => Object.setPrototypeOf(
|
|
||||||
Object.assign(() => {}, self, options),
|
|
||||||
Object.getPrototypeOf(self),
|
|
||||||
))
|
|
||||||
|
|
||||||
export const withRuntime: {
|
|
||||||
<P extends {}, A extends React.ReactNode, E, R, F extends Component.Signature>(
|
|
||||||
context: React.Context<Context.Context<R>>,
|
|
||||||
): (self: Component<P, A, E, Scope.Scope | NoInfer<R>, F>) => F
|
|
||||||
<P extends {}, A extends React.ReactNode, E, R, F extends Component.Signature>(
|
|
||||||
self: Component<P, A, E, Scope.Scope | NoInfer<R>, F>,
|
|
||||||
context: React.Context<Context.Context<R>>,
|
|
||||||
): F
|
|
||||||
} = Function.dual(2, <P extends {}, A extends React.ReactNode, E, R, F extends Component.Signature>(
|
|
||||||
self: Component<P, A, E, Scope.Scope | R, F>,
|
|
||||||
context: React.Context<Context.Context<R>>,
|
|
||||||
) => function WithRuntime(props: P) {
|
|
||||||
return React.createElement(
|
|
||||||
Effect.runSyncWith(React.useContext(context))(self.use) as React.FC<P>,
|
|
||||||
props,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export declare namespace useScope {
|
|
||||||
export interface Options {
|
|
||||||
readonly finalizerExecutionStrategy?: "sequential" | "parallel"
|
|
||||||
readonly finalizerExecutionDebounce?: Duration.Input
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useScope = Effect.fnUntraced(function* (
|
|
||||||
deps: React.DependencyList,
|
|
||||||
options?: useScope.Options,
|
|
||||||
): Effect.fn.Return<Scope.Scope> {
|
|
||||||
const context = yield* Effect.context<never>()
|
|
||||||
const contextRef = React.useRef(context)
|
|
||||||
contextRef.current = context
|
|
||||||
const scope = React.useMemo(
|
|
||||||
() => Scope.makeUnsafe(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy),
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: caller controls scope lifetime
|
|
||||||
deps,
|
|
||||||
)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const pending = scopeCleanupTimers.get(scope)
|
|
||||||
if (pending !== undefined) clearTimeout(pending)
|
|
||||||
return () => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
Effect.runSyncWith(contextRef.current)(Scope.close(scope, Exit.succeed(undefined)))
|
|
||||||
scopeCleanupTimers.delete(scope)
|
|
||||||
}, durationMillis(options?.finalizerExecutionDebounce ?? defaultOptions.finalizerExecutionDebounce))
|
|
||||||
scopeCleanupTimers.set(scope, timer)
|
|
||||||
}
|
|
||||||
}, [scope, options?.finalizerExecutionDebounce])
|
|
||||||
|
|
||||||
return scope
|
|
||||||
})
|
|
||||||
|
|
||||||
const scopeCleanupTimers = new WeakMap<object, ReturnType<typeof setTimeout>>()
|
|
||||||
|
|
||||||
const durationMillis = (input: Duration.Input): number => {
|
|
||||||
if (typeof input === "number") return input
|
|
||||||
return Number(Effect.runSync(Effect.map(Effect.succeed(input), value => {
|
|
||||||
const match = typeof value === "string" ? /([\d.]+)\s*(millis|seconds?)/.exec(value) : undefined
|
|
||||||
if (!match) return 0
|
|
||||||
return Number(match[1]) * (match[2].startsWith("second") ? 1_000 : 1)
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useOnMount = Effect.fnUntraced(function* <A, E, R>(
|
|
||||||
f: () => Effect.Effect<A, E, R>,
|
|
||||||
): Effect.fn.Return<A, E, R> {
|
|
||||||
const context = yield* Effect.context<R>()
|
|
||||||
const id = React.useId()
|
|
||||||
let cached = mountCache.get(id)
|
|
||||||
if (cached === undefined) {
|
|
||||||
cached = {
|
|
||||||
effect: Effect.runSyncWith(context)(Effect.cached(f())),
|
|
||||||
}
|
|
||||||
mountCache.set(id, cached)
|
|
||||||
}
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (cached?.cleanup !== undefined) clearTimeout(cached.cleanup)
|
|
||||||
const entry = cached
|
|
||||||
return () => {
|
|
||||||
entry.cleanup = setTimeout(() => mountCache.delete(id), 0)
|
|
||||||
}
|
|
||||||
}, [id, cached])
|
|
||||||
return yield* cached.effect as Effect.Effect<A, E, R>
|
|
||||||
})
|
|
||||||
|
|
||||||
const mountCache = new Map<string, {
|
|
||||||
readonly effect: Effect.Effect<unknown, unknown, unknown>
|
|
||||||
cleanup?: ReturnType<typeof setTimeout>
|
|
||||||
}>()
|
|
||||||
|
|
||||||
export declare namespace useOnChange {
|
|
||||||
export interface Options extends useScope.Options {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useOnChange = Effect.fnUntraced(function* <A, E, R>(
|
|
||||||
f: () => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
options?: useOnChange.Options,
|
|
||||||
): Effect.fn.Return<A, E, Exclude<R, Scope.Scope>> {
|
|
||||||
const context = yield* Effect.context<Exclude<R, Scope.Scope>>()
|
|
||||||
const scope = yield* useScope(deps, options)
|
|
||||||
const cached =
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: scope tracks the caller-provided dependency list
|
|
||||||
React.useMemo(
|
|
||||||
() => Effect.runSyncWith(context)(Effect.cached(Effect.provideService(f(), Scope.Scope, scope))),
|
|
||||||
[scope],
|
|
||||||
)
|
|
||||||
return yield* cached
|
|
||||||
})
|
|
||||||
|
|
||||||
export declare namespace useReactEffect {
|
|
||||||
export interface Options {
|
|
||||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
|
||||||
readonly finalizerExecutionStrategy?: "sequential" | "parallel"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runReactEffect = <E, R>(
|
|
||||||
context: Context.Context<Exclude<R, Scope.Scope>>,
|
|
||||||
f: () => Effect.Effect<void, E, R>,
|
|
||||||
options?: useReactEffect.Options,
|
|
||||||
): (() => void) => {
|
|
||||||
const scope = Scope.makeUnsafe(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy)
|
|
||||||
Effect.runSyncWith(context)(Effect.exit(Effect.provideService(f(), Scope.Scope, scope)))
|
|
||||||
return () => {
|
|
||||||
const close = Scope.close(scope, Exit.succeed(undefined))
|
|
||||||
if ((options?.finalizerExecutionMode ?? "fork") === "sync") Effect.runSyncWith(context)(close)
|
|
||||||
else Effect.runForkWith(context)(close)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useReactEffect = Effect.fnUntraced(function* <E, R>(
|
|
||||||
f: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: useReactEffect.Options,
|
|
||||||
): Effect.fn.Return<void, never, Exclude<R, Scope.Scope>> {
|
|
||||||
const context = yield* Effect.context<Exclude<R, Scope.Scope>>()
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: caller supplies React dependencies
|
|
||||||
React.useEffect(() => runReactEffect(context, f, options), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export declare namespace useReactLayoutEffect {
|
|
||||||
export interface Options extends useReactEffect.Options {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useReactLayoutEffect = Effect.fnUntraced(function* <E, R>(
|
|
||||||
f: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: useReactLayoutEffect.Options,
|
|
||||||
): Effect.fn.Return<void, never, Exclude<R, Scope.Scope>> {
|
|
||||||
const context = yield* Effect.context<Exclude<R, Scope.Scope>>()
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: caller supplies React dependencies
|
|
||||||
React.useLayoutEffect(() => runReactEffect(context, f, options), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useRunSync = <R = never>(): Effect.Effect<
|
|
||||||
<A, E = never>(effect: Effect.Effect<A, E, R>) => A,
|
|
||||||
never,
|
|
||||||
R
|
|
||||||
> => Effect.map(Effect.context<R>(), Effect.runSyncWith)
|
|
||||||
|
|
||||||
export const useRunPromise = <R = never>(): Effect.Effect<
|
|
||||||
<A, E = never>(effect: Effect.Effect<A, E, R>) => Promise<A>,
|
|
||||||
never,
|
|
||||||
R
|
|
||||||
> => Effect.map(Effect.context<R>(), Effect.runPromiseWith)
|
|
||||||
|
|
||||||
export const useCallbackSync = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
|
||||||
f: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.fn.Return<(...args: Args) => A, never, R> {
|
|
||||||
const context = yield* Effect.context<R>()
|
|
||||||
const contextRef = React.useRef(context)
|
|
||||||
contextRef.current = context
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: caller supplies React dependencies
|
|
||||||
return React.useCallback((...args: Args) => Effect.runSyncWith(contextRef.current)(f(...args)), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useCallbackPromise = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
|
||||||
f: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.fn.Return<(...args: Args) => Promise<A>, never, R> {
|
|
||||||
const context = yield* Effect.context<R>()
|
|
||||||
const contextRef = React.useRef(context)
|
|
||||||
contextRef.current = context
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: caller supplies React dependencies
|
|
||||||
return React.useCallback((...args: Args) => Effect.runPromiseWith(contextRef.current)(f(...args)), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export declare namespace useContext {
|
|
||||||
export interface Options extends useOnChange.Options {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useContextFromLayer = <ROut, E, RIn>(
|
|
||||||
layer: Layer.Layer<ROut, E, RIn>,
|
|
||||||
options?: useContext.Options,
|
|
||||||
): Effect.Effect<Context.Context<ROut>, E, RIn | Scope.Scope> => useOnChange(
|
|
||||||
() => Effect.flatMap(
|
|
||||||
Effect.context<RIn>(),
|
|
||||||
context => Layer.build(Layer.provide(layer, Layer.succeedContext(context))),
|
|
||||||
),
|
|
||||||
[layer],
|
|
||||||
options,
|
|
||||||
)
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */
|
/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */
|
||||||
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||||
import { Context, type Duration, Effect, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, identity, Layer, Option, Pipeable, Predicate, Ref, Runtime, Scope, Tracer } from "effect"
|
import { Context, type Duration, Effect, Equivalence, Exit, Fiber, Function, HashMap, identity, Layer, Option, Pipeable, Predicate, Ref, Scope, Tracer } from "effect"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ export const ComponentImplPrototype: ComponentImplPrototype<any, any> = Object.f
|
|||||||
contextRef: React.RefObject<Context.Context<Exclude<R, Scope.Scope>>>,
|
contextRef: React.RefObject<Context.Context<Exclude<R, Scope.Scope>>>,
|
||||||
) {
|
) {
|
||||||
return (props: P) => Effect.runSyncWith(contextRef.current)(
|
return (props: P) => Effect.runSyncWith(contextRef.current)(
|
||||||
Effect.andThen(
|
Effect.flatMap(
|
||||||
useScope([], this),
|
useScope([], this),
|
||||||
scope => Effect.provideService(this.body(props), Scope.Scope, scope),
|
scope => Effect.provideService(this.body(props), Scope.Scope, scope),
|
||||||
)
|
)
|
||||||
@@ -83,16 +83,26 @@ const use = Effect.fnUntraced(function* <P extends {}, A extends React.ReactNode
|
|||||||
const contextRef = React.useRef<Context.Context<Exclude<R, Scope.Scope>>>(null!)
|
const contextRef = React.useRef<Context.Context<Exclude<R, Scope.Scope>>>(null!)
|
||||||
contextRef.current = yield* Effect.context<Exclude<R, Scope.Scope>>()
|
contextRef.current = yield* Effect.context<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
return yield* React.useState(() => Effect.runSyncWith(contextRef.current)(Effect.cachedFunction(
|
const componentRef = React.useRef<F | null>(null)
|
||||||
(_services: readonly any[]) => Effect.sync(() => {
|
const previousServicesRef = React.useRef<readonly any[] | null>(null)
|
||||||
|
|
||||||
|
const services = Array.from(
|
||||||
|
Context.omit(...self.nonReactiveTags)(contextRef.current).mapUnsafe.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!componentRef.current ||
|
||||||
|
!previousServicesRef.current ||
|
||||||
|
services.length !== previousServicesRef.current.length ||
|
||||||
|
!Equivalence.Array(Equivalence.strictEqual())(services, previousServicesRef.current)
|
||||||
|
) {
|
||||||
|
previousServicesRef.current = services
|
||||||
const f = self.asFunctionComponent(contextRef)
|
const f = self.asFunctionComponent(contextRef)
|
||||||
self.setFunctionComponentName(f)
|
self.setFunctionComponentName(f)
|
||||||
return self.transformFunctionComponent(f)
|
componentRef.current = self.transformFunctionComponent(f)
|
||||||
}),
|
}
|
||||||
Equivalence.array(Equivalence.strictEqual()),
|
|
||||||
)))[0](Array.from(
|
return componentRef.current
|
||||||
Context.omit(...self.nonReactiveTags)(contextRef.current).mapUnsafe.values()
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -130,9 +140,9 @@ export interface ComponentOptions {
|
|||||||
* Specifies the execution strategy for finalizers when the component unmounts or its scope closes.
|
* Specifies the execution strategy for finalizers when the component unmounts or its scope closes.
|
||||||
* Determines whether finalizers execute sequentially or in parallel.
|
* Determines whether finalizers execute sequentially or in parallel.
|
||||||
*
|
*
|
||||||
* @default ExecutionStrategy.sequential
|
* @default "sequential"
|
||||||
*/
|
*/
|
||||||
readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy
|
readonly finalizerExecutionStrategy: "sequential" | "parallel"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounce duration before executing finalizers after component unmount.
|
* Debounce duration before executing finalizers after component unmount.
|
||||||
@@ -146,7 +156,7 @@ export interface ComponentOptions {
|
|||||||
|
|
||||||
export const defaultOptions: ComponentOptions = {
|
export const defaultOptions: ComponentOptions = {
|
||||||
nonReactiveTags: [Tracer.ParentSpan],
|
nonReactiveTags: [Tracer.ParentSpan],
|
||||||
finalizerExecutionStrategy: ExecutionStrategy.sequential,
|
finalizerExecutionStrategy: "sequential",
|
||||||
finalizerExecutionDebounce: "100 millis",
|
finalizerExecutionDebounce: "100 millis",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,18 +619,18 @@ export const withOptions: {
|
|||||||
*/
|
*/
|
||||||
export const withRuntime: {
|
export const withRuntime: {
|
||||||
<P extends {}, A extends React.ReactNode, E, R, F extends Component.Signature>(
|
<P extends {}, A extends React.ReactNode, E, R, F extends Component.Signature>(
|
||||||
context: React.Context<Runtime.Runtime<R>>,
|
context: React.Context<Context.Context<R>>,
|
||||||
): (self: Component<P, A, E, Scope.Scope | NoInfer<R>, F>) => F
|
): (self: Component<P, A, E, Scope.Scope | NoInfer<R>, F>) => F
|
||||||
<P extends {}, A extends React.ReactNode, E, R, F extends Component.Signature>(
|
<P extends {}, A extends React.ReactNode, E, R, F extends Component.Signature>(
|
||||||
self: Component<P, A, E, Scope.Scope | NoInfer<R>, F>,
|
self: Component<P, A, E, Scope.Scope | NoInfer<R>, F>,
|
||||||
context: React.Context<Runtime.Runtime<R>>,
|
context: React.Context<Context.Context<R>>,
|
||||||
): F
|
): F
|
||||||
} = Function.dual(2, <P extends {}, A extends React.ReactNode, E, R, F extends Component.Signature>(
|
} = Function.dual(2, <P extends {}, A extends React.ReactNode, E, R, F extends Component.Signature>(
|
||||||
self: Component<P, A, E, R, F>,
|
self: Component<P, A, E, R, F>,
|
||||||
context: React.Context<Runtime.Runtime<R>>,
|
context: React.Context<Context.Context<R>>,
|
||||||
) => function WithRuntime(props: P) {
|
) => function WithRuntime(props: P) {
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
Runtime.runSync(React.useContext(context))(self.use) as React.FC<P>,
|
Effect.runSyncWith(React.useContext(context))(self.use) as React.FC<P>,
|
||||||
props,
|
props,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -653,7 +663,7 @@ export declare namespace ScopeMap {
|
|||||||
|
|
||||||
export declare namespace useScope {
|
export declare namespace useScope {
|
||||||
export interface Options {
|
export interface Options {
|
||||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
readonly finalizerExecutionStrategy?: "sequential" | "parallel"
|
||||||
readonly finalizerExecutionDebounce?: Duration.Input
|
readonly finalizerExecutionDebounce?: Duration.Input
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -805,7 +815,7 @@ export const useOnChange = Effect.fnUntraced(function* <A, E, R>(
|
|||||||
export declare namespace useReactEffect {
|
export declare namespace useReactEffect {
|
||||||
export interface Options {
|
export interface Options {
|
||||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
readonly finalizerExecutionStrategy?: "sequential" | "parallel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Array, type Cause, Chunk, type Duration, Effect, Equal, Function, ident
|
|||||||
import type * as React from "react"
|
import type * as React from "react"
|
||||||
import * as Component from "./Component.js"
|
import * as Component from "./Component.js"
|
||||||
import * as Lens from "./Lens.js"
|
import * as Lens from "./Lens.js"
|
||||||
import * as Subscribable from "./Subscribable.js"
|
import * as View from "./View.js"
|
||||||
|
|
||||||
|
|
||||||
export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form")
|
export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form")
|
||||||
@@ -18,12 +18,12 @@ extends Pipeable.Pipeable {
|
|||||||
readonly [FormTypeId]: FormTypeId
|
readonly [FormTypeId]: FormTypeId
|
||||||
|
|
||||||
readonly path: P
|
readonly path: P
|
||||||
readonly value: Subscribable.Subscribable<Option.Option<A>, ER, never>
|
readonly value: View.View<Option.Option<A>, ER, never>
|
||||||
readonly encodedValue: Lens.Lens<I, ER, EW, never, never>
|
readonly encodedValue: Lens.Lens<I, ER, EW, never, never>
|
||||||
readonly issues: Subscribable.Subscribable<readonly FormIssue[], never, never>
|
readonly issues: View.View<readonly FormIssue[], never, never>
|
||||||
readonly isValidating: Subscribable.Subscribable<boolean, never, never>
|
readonly isValidating: View.View<boolean, never, never>
|
||||||
readonly canCommit: Subscribable.Subscribable<boolean, never, never>
|
readonly canCommit: View.View<boolean, never, never>
|
||||||
readonly isCommitting: Subscribable.Subscribable<boolean, never, never>
|
readonly isCommitting: View.View<boolean, never, never>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FormImpl<out P extends readonly PropertyKey[], in out A, in out I = A, in out ER = never, in out EW = never>
|
export class FormImpl<out P extends readonly PropertyKey[], in out A, in out I = A, in out ER = never, in out EW = never>
|
||||||
@@ -32,12 +32,12 @@ extends Pipeable.Class implements Form<P, A, I, ER, EW> {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly path: P,
|
readonly path: P,
|
||||||
readonly value: Subscribable.Subscribable<Option.Option<A>, ER, never>,
|
readonly value: View.View<Option.Option<A>, ER, never>,
|
||||||
readonly encodedValue: Lens.Lens<I, ER, EW, never, never>,
|
readonly encodedValue: Lens.Lens<I, ER, EW, never, never>,
|
||||||
readonly issues: Subscribable.Subscribable<readonly FormIssue[], never, never>,
|
readonly issues: View.View<readonly FormIssue[], never, never>,
|
||||||
readonly isValidating: Subscribable.Subscribable<boolean, never, never>,
|
readonly isValidating: View.View<boolean, never, never>,
|
||||||
readonly canCommit: Subscribable.Subscribable<boolean, never, never>,
|
readonly canCommit: View.View<boolean, never, never>,
|
||||||
readonly isCommitting: Subscribable.Subscribable<boolean, never, never>,
|
readonly isCommitting: View.View<boolean, never, never>,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@@ -71,9 +71,9 @@ export const focusObjectOn: {
|
|||||||
|
|
||||||
return new FormImpl(
|
return new FormImpl(
|
||||||
path,
|
path,
|
||||||
Subscribable.mapOption(form.value, a => a[key]),
|
View.mapOption(form.value, a => a[key]),
|
||||||
Lens.focusObjectOn(form.encodedValue, key),
|
Lens.focusObjectOn(form.encodedValue, key),
|
||||||
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
|
View.map(form.issues, issues => filterIssuesByPath(issues, path)),
|
||||||
form.isValidating,
|
form.isValidating,
|
||||||
form.canCommit,
|
form.canCommit,
|
||||||
form.isCommitting,
|
form.isCommitting,
|
||||||
@@ -97,9 +97,9 @@ export const focusArrayAt: {
|
|||||||
|
|
||||||
return new FormImpl(
|
return new FormImpl(
|
||||||
path,
|
path,
|
||||||
Subscribable.mapOptionEffect(form.value, values => Effect.fromOption(Array.get(values, index))),
|
View.mapOptionEffect(form.value, values => Effect.fromOption(Array.get(values, index))),
|
||||||
Lens.focusArrayAt(form.encodedValue, index),
|
Lens.focusArrayAt(form.encodedValue, index),
|
||||||
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
|
View.map(form.issues, issues => filterIssuesByPath(issues, path)),
|
||||||
form.isValidating,
|
form.isValidating,
|
||||||
form.canCommit,
|
form.canCommit,
|
||||||
form.isCommitting,
|
form.isCommitting,
|
||||||
@@ -123,9 +123,9 @@ export const focusTupleAt: {
|
|||||||
|
|
||||||
return new FormImpl(
|
return new FormImpl(
|
||||||
path,
|
path,
|
||||||
Subscribable.mapOption(form.value, values => values[index]),
|
View.mapOption(form.value, values => values[index]),
|
||||||
Lens.focusTupleAt(form.encodedValue, index),
|
Lens.focusTupleAt(form.encodedValue, index),
|
||||||
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
|
View.map(form.issues, issues => filterIssuesByPath(issues, path)),
|
||||||
form.isValidating,
|
form.isValidating,
|
||||||
form.canCommit,
|
form.canCommit,
|
||||||
form.isCommitting,
|
form.isCommitting,
|
||||||
@@ -149,9 +149,9 @@ export const focusChunkAt: {
|
|||||||
|
|
||||||
return new FormImpl(
|
return new FormImpl(
|
||||||
path,
|
path,
|
||||||
Subscribable.mapOptionEffect(form.value, values => Effect.fromOption(Chunk.get(values, index))),
|
View.mapOptionEffect(form.value, values => Effect.fromOption(Chunk.get(values, index))),
|
||||||
Lens.focusChunkAt(form.encodedValue, index),
|
Lens.focusChunkAt(form.encodedValue, index),
|
||||||
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
|
View.map(form.issues, issues => filterIssuesByPath(issues, path)),
|
||||||
form.isValidating,
|
form.isValidating,
|
||||||
form.canCommit,
|
form.canCommit,
|
||||||
form.isCommitting,
|
form.isCommitting,
|
||||||
|
|||||||
@@ -1,146 +1,153 @@
|
|||||||
import { type Context, Effect, Equal, type Fiber, Option, Pipeable, Predicate, type Scope, Stream, SubscriptionRef } from "effect"
|
import { type Context, Effect, Equal, Exit, type Fiber, Option, Pipeable, Predicate, type Scope, Stream, SubscriptionRef } from "effect"
|
||||||
import { Subscribable } from "effect-lens"
|
import { AsyncResult } from "effect/unstable/reactivity"
|
||||||
import * as Result from "./Result.js"
|
import * as Lens from "./Lens.js"
|
||||||
|
import * as View from "./View.js"
|
||||||
|
|
||||||
|
|
||||||
export const MutationTypeId: unique symbol = Symbol.for("@effect-fc/Mutation/Mutation")
|
export const MutationTypeId: unique symbol = Symbol.for("@effect-fc/Mutation/Mutation")
|
||||||
export type MutationTypeId = typeof MutationTypeId
|
export type MutationTypeId = typeof MutationTypeId
|
||||||
|
|
||||||
export interface Mutation<in out K extends Mutation.AnyKey, in out A, in out E = never, in out R = never, in out P = never>
|
export interface Mutation<in out K, out A, out E = never, in out R = never>
|
||||||
extends Pipeable.Pipeable {
|
extends Pipeable.Pipeable {
|
||||||
readonly [MutationTypeId]: MutationTypeId
|
readonly [MutationTypeId]: MutationTypeId
|
||||||
|
|
||||||
readonly context: Context.Context<Scope.Scope | R>
|
readonly context: Context.Context<Scope.Scope | R>
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, R>
|
readonly f: (key: K) => Effect.Effect<A, E, R>
|
||||||
readonly initialProgress: P
|
|
||||||
|
|
||||||
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
readonly latestKey: View.View<Option.Option<K>>
|
||||||
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
readonly fiber: View.View<Option.Option<Fiber.Fiber<A, E>>>
|
||||||
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
readonly state: View.View<AsyncResult.AsyncResult<A, E>>
|
||||||
readonly latestFinalResult: Subscribable.Subscribable<Option.Option<Result.Final<A, E, P>>>
|
readonly latestFinalResult: View.View<Option.Option<AsyncResult.Success<A, E> | AsyncResult.Failure<A, E>>>
|
||||||
|
|
||||||
mutate(key: K): Effect.Effect<Result.Final<A, E, P>>
|
mutate(key: K): Effect.Effect<AsyncResult.Success<A, E> | AsyncResult.Failure<A, E>>
|
||||||
mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
mutateView(key: K): Effect.Effect<View.View<AsyncResult.AsyncResult<A, E>>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare namespace Mutation {
|
export const isMutation = (u: unknown): u is Mutation<unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, MutationTypeId)
|
||||||
export type AnyKey = readonly any[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MutationImpl<in out K extends Mutation.AnyKey, in out A, in out E = never, in out R = never, in out P = never>
|
|
||||||
extends Pipeable.Class implements Mutation<K, A, E, R, P> {
|
export class MutationImpl<in out K, in out A, in out E = never, in out R = never>
|
||||||
|
extends Pipeable.Class implements Mutation<K, A, E, R> {
|
||||||
readonly [MutationTypeId]: MutationTypeId = MutationTypeId
|
readonly [MutationTypeId]: MutationTypeId = MutationTypeId
|
||||||
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
|
||||||
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
|
||||||
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
|
||||||
readonly latestFinalResult: Subscribable.Subscribable<Option.Option<Result.Final<A, E, P>>>
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly context: Context.Context<Scope.Scope | R>,
|
readonly context: Context.Context<Scope.Scope | R>,
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
||||||
readonly initialProgress: P,
|
|
||||||
|
|
||||||
readonly latestKeyRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
readonly latestKey: Lens.Lens<Option.Option<K>>,
|
||||||
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
readonly fiber: Lens.Lens<Option.Option<Fiber.Fiber<A, E>>>,
|
||||||
readonly resultRef: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
readonly state: Lens.Lens<AsyncResult.AsyncResult<A, E>>,
|
||||||
readonly latestFinalResultRef: SubscriptionRef.SubscriptionRef<Option.Option<Result.Final<A, E, P>>>,
|
readonly latestFinalResult: Lens.Lens<Option.Option<AsyncResult.Success<A, E> | AsyncResult.Failure<A, E>>>,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.latestKey = fromSubscriptionRef(latestKeyRef)
|
|
||||||
this.fiber = fromSubscriptionRef(fiberRef)
|
|
||||||
this.result = fromSubscriptionRef(resultRef)
|
|
||||||
this.latestFinalResult = fromSubscriptionRef(latestFinalResultRef)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mutate(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
mutate(key: K): Effect.Effect<AsyncResult.Success<A, E> | AsyncResult.Failure<A, E>> {
|
||||||
return SubscriptionRef.set(this.latestKeyRef, Option.some(key)).pipe(
|
return Lens.set(this.latestKey, Option.some(key)).pipe(
|
||||||
Effect.andThen(this.start(key)),
|
Effect.andThen(this.start(key)),
|
||||||
Effect.andThen(sub => this.watch(sub)),
|
Effect.flatMap(state => this.watch(state)),
|
||||||
Effect.provide(this.context),
|
Effect.provide(this.context),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>> {
|
mutateView(key: K): Effect.Effect<View.View<AsyncResult.AsyncResult<A, E>>> {
|
||||||
return SubscriptionRef.set(this.latestKeyRef, Option.some(key)).pipe(
|
return Lens.set(this.latestKey, Option.some(key)).pipe(
|
||||||
Effect.andThen(this.start(key)),
|
Effect.andThen(this.start(key)),
|
||||||
Effect.tap(sub => Effect.forkScoped(this.watch(sub))),
|
Effect.tap(state => Effect.forkScoped(this.watch(state))),
|
||||||
Effect.provide(this.context),
|
Effect.provide(this.context),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
start(key: K): Effect.Effect<
|
start(key: K): Effect.Effect<
|
||||||
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
View.View<AsyncResult.AsyncResult<A, E>>,
|
||||||
never,
|
never,
|
||||||
Scope.Scope | R
|
Scope.Scope | R
|
||||||
> {
|
> {
|
||||||
const self = this
|
return Effect.gen({ self: this }, function*() {
|
||||||
return Effect.gen(function*() {
|
const previous = yield* Lens.get(this.latestFinalResult)
|
||||||
const initial = yield* SubscriptionRef.get(self.latestFinalResultRef)
|
const state = Lens.fromSubscriptionRef(yield* SubscriptionRef.make<AsyncResult.AsyncResult<A, E>>(
|
||||||
const [sub, fiber] = yield* Result.unsafeForkEffect<A, E, R, P>(
|
Option.getOrElse(previous, () => AsyncResult.initial(false))
|
||||||
Effect.onExit(self.f(key), () => Effect.andThen(
|
))
|
||||||
Effect.all([Effect.fiberId, SubscriptionRef.get(self.fiberRef)]),
|
|
||||||
([currentFiberId, fiber]) => Option.match(fiber, {
|
|
||||||
onSome: v => Equal.equals(currentFiberId, v.id)
|
|
||||||
? SubscriptionRef.set(self.fiberRef, Option.none())
|
|
||||||
: Effect.succeed(undefined),
|
|
||||||
onNone: () => Effect.succeed(undefined),
|
|
||||||
}),
|
|
||||||
)),
|
|
||||||
|
|
||||||
{
|
const fiber = yield* Effect.forkScoped(Effect.andThen(
|
||||||
initial: Option.isSome(initial) ? Result.willFetch(initial.value) : Result.initial(),
|
Lens.update(state, AsyncResult.match({
|
||||||
initialProgress: self.initialProgress,
|
onInitial: () => AsyncResult.initial(true),
|
||||||
} as Result.unsafeForkEffect.Options<A, E, P>,
|
onSuccess: v => AsyncResult.success(v.value, {
|
||||||
)
|
waiting: true,
|
||||||
yield* SubscriptionRef.set(self.fiberRef, Option.some(fiber))
|
}),
|
||||||
return sub
|
onFailure: v => AsyncResult.failure(v.cause, {
|
||||||
|
waiting: true,
|
||||||
|
previousSuccess: v.previousSuccess,
|
||||||
|
})
|
||||||
|
})),
|
||||||
|
|
||||||
|
Effect.onExit(this.f(key), exit => Lens.update(
|
||||||
|
state,
|
||||||
|
previous => Exit.match(exit, {
|
||||||
|
onSuccess: v => AsyncResult.success(v),
|
||||||
|
onFailure: c => AsyncResult.match(previous, {
|
||||||
|
onInitial: () => AsyncResult.failure(c),
|
||||||
|
onSuccess: v => AsyncResult.failure(c, {
|
||||||
|
previousSuccess: Option.some(v),
|
||||||
|
}),
|
||||||
|
onFailure: v => AsyncResult.failure(c, {
|
||||||
|
previousSuccess: v.previousSuccess,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
).pipe(
|
||||||
|
Effect.andThen(Effect.all([
|
||||||
|
Effect.fiberId,
|
||||||
|
Lens.get(this.fiber),
|
||||||
|
])),
|
||||||
|
Effect.flatMap(([fiberId, fiber]) => Option.match(fiber, {
|
||||||
|
onSome: v => Equal.equals(fiberId, v.id)
|
||||||
|
? Lens.set(this.fiber, Option.none())
|
||||||
|
: Effect.void,
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
})),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
|
||||||
|
yield* Lens.set(this.fiber, Option.some(fiber))
|
||||||
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
sub: Subscribable.Subscribable<Result.Result<A, E, P>>
|
state: View.View<AsyncResult.AsyncResult<A, E>>
|
||||||
): Effect.Effect<Result.Final<A, E, P>> {
|
): Effect.Effect<AsyncResult.Success<A, E> | AsyncResult.Failure<A, E>> {
|
||||||
return sub.get.pipe(
|
return View.get(state).pipe(
|
||||||
Effect.andThen(initial => Stream.runFoldEffect(
|
Effect.andThen(initial => Stream.runFoldEffect(
|
||||||
Stream.takeUntil(sub.changes, result => Result.isFinal(result) && !Result.hasFlag(result)),
|
View.changes(state),
|
||||||
() => initial,
|
() => initial,
|
||||||
(_, result) => Effect.as(SubscriptionRef.set(this.resultRef, result), result),
|
(_, result) => Effect.as(Lens.set(this.state, result), result),
|
||||||
) as Effect.Effect<Result.Final<A, E, P>>),
|
) as Effect.Effect<AsyncResult.Success<A, E> | AsyncResult.Failure<A, E>>),
|
||||||
Effect.tap(result => SubscriptionRef.set(this.latestFinalResultRef, Option.some(result))),
|
Effect.tap(result => Lens.set(this.latestFinalResult, Option.some(result))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const isMutation = (u: unknown): u is Mutation<readonly unknown[], unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, MutationTypeId)
|
|
||||||
|
|
||||||
|
|
||||||
export declare namespace make {
|
export declare namespace make {
|
||||||
export interface Options<K extends Mutation.AnyKey = never, A = void, E = never, R = never, P = never> {
|
export interface Options<K = never, A = void, E = never, R = never> {
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
|
readonly f: (key: K) => Effect.Effect<A, E, R>
|
||||||
readonly initialProgress?: P
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const make = Effect.fnUntraced(function* <const K extends Mutation.AnyKey = never, A = void, E = never, R = never, P = never>(
|
export const make = Effect.fnUntraced(function* <K = never, A = void, E = never, R = never>(
|
||||||
options: make.Options<K, A, E, R, P>
|
options: make.Options<K, A, E, R>
|
||||||
): Effect.fn.Return<
|
): Effect.fn.Return<
|
||||||
Mutation<K, A, E, Result.forkEffect.OutputContext<R, P>, P>,
|
Mutation<K, A, E, R>,
|
||||||
never,
|
never,
|
||||||
Scope.Scope | Result.forkEffect.OutputContext<R, P>
|
Scope.Scope | R
|
||||||
> {
|
> {
|
||||||
return new MutationImpl(
|
return new MutationImpl(
|
||||||
yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<R, P>>(),
|
yield* Effect.context<Scope.Scope | R>(),
|
||||||
options.f as any,
|
options.f,
|
||||||
options.initialProgress as P,
|
|
||||||
|
|
||||||
yield* SubscriptionRef.make(Option.none<K>()),
|
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<K>())),
|
||||||
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>())),
|
||||||
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
Lens.fromSubscriptionRef(yield* SubscriptionRef.make<AsyncResult.AsyncResult<A, E>>(AsyncResult.initial())),
|
||||||
yield* SubscriptionRef.make(Option.none<Result.Final<A, E, P>>()),
|
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<AsyncResult.Success<A, E> | AsyncResult.Failure<A, E>>())),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const fromSubscriptionRef = <A>(ref: SubscriptionRef.SubscriptionRef<A>): Subscribable.Subscribable<A> => Subscribable.make({
|
|
||||||
get: SubscriptionRef.get(ref),
|
|
||||||
changes: SubscriptionRef.changes(ref),
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,88 +1,106 @@
|
|||||||
import {
|
import { type Cause, type Context, Duration, Effect, Equal, type Equivalence, Exit, Fiber, Option, Pipeable, Predicate, type Scope, Semaphore, Stream, SubscriptionRef } from "effect"
|
||||||
type Cause,
|
import { AsyncResult } from "effect/unstable/reactivity"
|
||||||
type Context,
|
import * as Lens from "./Lens.js"
|
||||||
type Duration,
|
|
||||||
Effect,
|
|
||||||
Equal,
|
|
||||||
Fiber,
|
|
||||||
Option,
|
|
||||||
Pipeable,
|
|
||||||
Predicate,
|
|
||||||
type Scope,
|
|
||||||
Semaphore,
|
|
||||||
Stream,
|
|
||||||
SubscriptionRef,
|
|
||||||
} from "effect"
|
|
||||||
import { Subscribable } from "effect-lens"
|
|
||||||
import * as QueryClient from "./QueryClient.js"
|
import * as QueryClient from "./QueryClient.js"
|
||||||
import * as Result from "./Result.js"
|
import * as View from "./View.js"
|
||||||
|
|
||||||
|
|
||||||
export const QueryTypeId: unique symbol = Symbol.for("@effect-fc/Query/Query")
|
export const QueryTypeId: unique symbol = Symbol.for("@effect-fc/Query/Query")
|
||||||
export type QueryTypeId = typeof QueryTypeId
|
export type QueryTypeId = typeof QueryTypeId
|
||||||
|
|
||||||
export interface Query<in out K extends Query.AnyKey, in out A, in out KE = never, in out KR = never, in out E = never, in out R = never, in out P = never>
|
export interface Query<in out K, out A, out E = never, in out R = never>
|
||||||
extends Pipeable.Pipeable {
|
extends Pipeable.Pipeable {
|
||||||
readonly [QueryTypeId]: QueryTypeId
|
readonly [QueryTypeId]: QueryTypeId
|
||||||
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | KR | R>
|
|
||||||
readonly key: Stream.Stream<K, KE, KR>
|
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | R>
|
||||||
|
readonly key: View.View<K>
|
||||||
|
readonly keyEquivalence: Equivalence.Equivalence<K>
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, R>
|
readonly f: (key: K) => Effect.Effect<A, E, R>
|
||||||
readonly initialProgress: P
|
|
||||||
readonly staleTime: Duration.Input
|
readonly staleTime: Duration.Duration
|
||||||
readonly refreshOnWindowFocus: boolean
|
readonly refreshOnWindowFocus: boolean
|
||||||
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
|
||||||
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
readonly fiber: View.View<Option.Option<Fiber.Fiber<A, E>>>
|
||||||
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
readonly state: View.View<QueryState<K, A, E>>
|
||||||
readonly latestFinalResult: Subscribable.Subscribable<Option.Option<Result.Final<A, E, P>>>
|
readonly latestFinalState: View.View<Option.Option<FinalQueryState<K, A, E>>>
|
||||||
|
|
||||||
readonly run: Effect.Effect<void>
|
readonly run: Effect.Effect<void>
|
||||||
fetch(key: K): Effect.Effect<Result.Final<A, E, P>>
|
fetch(key: K): Effect.Effect<FinalQueryState<K, A, E>, Cause.NoSuchElementError>
|
||||||
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
fetchView(key: K): Effect.Effect<View.View<QueryState<K, A, E>>, Cause.NoSuchElementError>
|
||||||
readonly refresh: Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementError>
|
readonly refresh: Effect.Effect<FinalQueryState<K, A, E>, Cause.NoSuchElementError>
|
||||||
readonly refreshSubscribable: Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementError>
|
readonly refreshView: Effect.Effect<View.View<QueryState<K, A, E>>, Cause.NoSuchElementError>
|
||||||
|
|
||||||
readonly invalidateCache: Effect.Effect<void>
|
readonly invalidateCache: Effect.Effect<void>
|
||||||
invalidateCacheEntry(key: K): Effect.Effect<void>
|
invalidateCacheEntry(key: K): Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare namespace Query {
|
export interface QueryState<out K, out A, out E = never> {
|
||||||
export type AnyKey = readonly any[]
|
readonly key: K
|
||||||
|
readonly result: AsyncResult.AsyncResult<A, E>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueryImpl<in out K extends Query.AnyKey, in out A, in out KE = never, in out KR = never, in out E = never, in out R = never, in out P = never>
|
export interface FinalQueryState<out K, out A, out E = never> {
|
||||||
extends Pipeable.Class implements Query<K, A, KE, KR, E, R, P> {
|
readonly key: K
|
||||||
|
readonly result: AsyncResult.Success<A, E> | AsyncResult.Failure<A, E>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isQuery = (u: unknown): u is Query<unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, QueryTypeId)
|
||||||
|
|
||||||
|
|
||||||
|
export class QueryImpl<in out K, in out A, in out E = never, in out R = never>
|
||||||
|
extends Pipeable.Class implements Query<K, A, E, R> {
|
||||||
readonly [QueryTypeId]: QueryTypeId = QueryTypeId
|
readonly [QueryTypeId]: QueryTypeId = QueryTypeId
|
||||||
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
|
||||||
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
|
||||||
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
|
||||||
readonly latestFinalResult: Subscribable.Subscribable<Option.Option<Result.Final<A, E, P>>>
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | KR | R>,
|
readonly context: Context.Context<Scope.Scope | QueryClient.QueryClient | R>,
|
||||||
readonly key: Stream.Stream<K, KE, KR>,
|
readonly key: View.View<K>,
|
||||||
|
readonly keyEquivalence: Equivalence.Equivalence<K>,
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
||||||
readonly initialProgress: P,
|
|
||||||
readonly staleTime: Duration.Input,
|
readonly staleTime: Duration.Duration,
|
||||||
readonly refreshOnWindowFocus: boolean,
|
readonly refreshOnWindowFocus: boolean,
|
||||||
readonly latestKeyRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
|
||||||
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
readonly fiber: Lens.Lens<Option.Option<Fiber.Fiber<A, E>>>,
|
||||||
readonly resultRef: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
readonly state: Lens.Lens<QueryState<K, A, E>>,
|
||||||
readonly latestFinalResultRef: SubscriptionRef.SubscriptionRef<Option.Option<Result.Final<A, E, P>>>,
|
readonly latestFinalState: Lens.Lens<Option.Option<FinalQueryState<K, A, E>>>,
|
||||||
|
|
||||||
readonly runSemaphore: Semaphore.Semaphore,
|
readonly runSemaphore: Semaphore.Semaphore,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.latestKey = fromSubscriptionRef(latestKeyRef)
|
|
||||||
this.fiber = fromSubscriptionRef(fiberRef)
|
|
||||||
this.result = fromSubscriptionRef(resultRef)
|
|
||||||
this.latestFinalResult = fromSubscriptionRef(latestFinalResultRef)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get run(): Effect.Effect<void> {
|
get run(): Effect.Effect<void> {
|
||||||
const focus = this.refreshOnWindowFocus && typeof window !== "undefined"
|
|
||||||
? Stream.runForEach(Stream.fromEventListener<FocusEvent>(window, "focus"), () => this.refreshSubscribable)
|
|
||||||
: Effect.succeed(undefined)
|
|
||||||
return Effect.all([
|
return Effect.all([
|
||||||
Stream.runForEach(this.key, key => this.fetchSubscribable(key)),
|
Stream.runForEach(
|
||||||
focus,
|
this.key.changes,
|
||||||
], { concurrency: "unbounded", discard: true }).pipe(
|
key => Effect.gen({ self: this }, function*() {
|
||||||
|
yield* this.interrupt
|
||||||
|
const latestFinalState = yield* Lens.get(this.latestFinalState)
|
||||||
|
|
||||||
|
const state = yield* this.startCached(
|
||||||
|
Option.isSome(latestFinalState) && this.keyEquivalence(key, latestFinalState.value.key)
|
||||||
|
? latestFinalState.value
|
||||||
|
: {
|
||||||
|
key,
|
||||||
|
result: AsyncResult.initial(false),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* Effect.forkScoped(this.watch(state))
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
Effect.promise(() => import("@effect/platform-browser")).pipe(
|
||||||
|
Effect.flatMap(({ BrowserStream }) => this.refreshOnWindowFocus
|
||||||
|
? Stream.runForEach(
|
||||||
|
BrowserStream.fromEventListenerWindow("focus"),
|
||||||
|
() => this.refreshView,
|
||||||
|
)
|
||||||
|
: Effect.void
|
||||||
|
),
|
||||||
|
Effect.catchDefect(() => Effect.void),
|
||||||
|
),
|
||||||
|
], { concurrency: "unbounded" }).pipe(
|
||||||
Effect.ignore,
|
Effect.ignore,
|
||||||
this.runSemaphore.withPermits(1),
|
this.runSemaphore.withPermits(1),
|
||||||
Effect.provide(this.context),
|
Effect.provide(this.context),
|
||||||
@@ -90,194 +108,310 @@ extends Pipeable.Class implements Query<K, A, KE, KR, E, R, P> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get interrupt(): Effect.Effect<void> {
|
get interrupt(): Effect.Effect<void> {
|
||||||
return Effect.flatMap(SubscriptionRef.get(this.fiberRef), Option.match({
|
return Effect.flatMap(Lens.get(this.fiber), Option.match({
|
||||||
onSome: Fiber.interrupt,
|
onSome: Fiber.interrupt,
|
||||||
onNone: () => Effect.succeed(undefined),
|
onNone: () => Effect.void,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
fetch(key: K): Effect.Effect<FinalQueryState<K, A, E>, Cause.NoSuchElementError> {
|
||||||
const self = this
|
return Effect.gen({ self: this }, function*() {
|
||||||
return Effect.gen(function*() {
|
yield* this.interrupt
|
||||||
yield* self.interrupt
|
const state = yield* this.startCached({
|
||||||
yield* SubscriptionRef.set(self.latestKeyRef, Option.some(key))
|
key,
|
||||||
const previous = yield* SubscriptionRef.get(self.latestFinalResultRef)
|
result: AsyncResult.initial(false),
|
||||||
const sub = yield* self.startCached(key, Option.isSome(previous)
|
})
|
||||||
? Result.willFetch(previous.value) as Result.Final<A, E, P>
|
return yield* this.watch(state)
|
||||||
: Result.initial())
|
}).pipe(
|
||||||
return yield* self.watch(key, sub)
|
Effect.provide(this.context),
|
||||||
}).pipe(Effect.provide(this.context))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>> {
|
fetchView(key: K): Effect.Effect<
|
||||||
const self = this
|
View.View<QueryState<K, A, E>>,
|
||||||
return Effect.gen(function*() {
|
Cause.NoSuchElementError
|
||||||
yield* self.interrupt
|
> {
|
||||||
yield* SubscriptionRef.set(self.latestKeyRef, Option.some(key))
|
return Effect.gen({ self: this }, function*() {
|
||||||
const previous = yield* SubscriptionRef.get(self.latestFinalResultRef)
|
yield* this.interrupt
|
||||||
const sub = yield* self.startCached(key, Option.isSome(previous)
|
const state = yield* this.startCached({
|
||||||
? Result.willFetch(previous.value) as Result.Final<A, E, P>
|
key,
|
||||||
: Result.initial())
|
result: AsyncResult.initial(false),
|
||||||
yield* Effect.forkScoped(self.watch(key, sub))
|
})
|
||||||
return sub
|
|
||||||
}).pipe(Effect.provide(this.context))
|
yield* Effect.forkScoped(this.watch(state))
|
||||||
|
return state
|
||||||
|
}).pipe(
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get refresh(): Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementError> {
|
get refresh(): Effect.Effect<FinalQueryState<K, A, E>, Cause.NoSuchElementError> {
|
||||||
const self = this
|
return Effect.gen({ self: this }, function*() {
|
||||||
return Effect.gen(function*() {
|
yield* this.interrupt
|
||||||
yield* self.interrupt
|
const latestState = yield* Lens.get(this.state)
|
||||||
const key = yield* Effect.fromOption(yield* SubscriptionRef.get(self.latestKeyRef))
|
const latestFinalState = yield* Lens.get(this.latestFinalState)
|
||||||
const previous = yield* SubscriptionRef.get(self.latestFinalResultRef)
|
|
||||||
const sub = yield* self.startCached(key, Option.isSome(previous)
|
const state = yield* this.startCached(
|
||||||
? Result.willRefresh(previous.value) as Result.Final<A, E, P>
|
Option.isSome(latestFinalState) && this.keyEquivalence(latestState.key, latestFinalState.value.key)
|
||||||
: Result.initial())
|
? latestFinalState.value
|
||||||
return yield* self.watch(key, sub)
|
: {
|
||||||
}).pipe(Effect.provide(this.context))
|
key: latestState.key,
|
||||||
|
result: AsyncResult.initial(false),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return yield* this.watch(state)
|
||||||
|
}).pipe(
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get refreshSubscribable(): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementError> {
|
get refreshView(): Effect.Effect<
|
||||||
const self = this
|
View.View<QueryState<K, A, E>>,
|
||||||
return Effect.gen(function*() {
|
Cause.NoSuchElementError
|
||||||
yield* self.interrupt
|
> {
|
||||||
const key = yield* Effect.fromOption(yield* SubscriptionRef.get(self.latestKeyRef))
|
return Effect.gen({ self: this }, function*() {
|
||||||
const previous = yield* SubscriptionRef.get(self.latestFinalResultRef)
|
yield* this.interrupt
|
||||||
const sub = yield* self.startCached(key, Option.isSome(previous)
|
const latestState = yield* Lens.get(this.state)
|
||||||
? Result.willRefresh(previous.value) as Result.Final<A, E, P>
|
const latestFinalState = yield* Lens.get(this.latestFinalState)
|
||||||
: Result.initial())
|
|
||||||
yield* Effect.forkScoped(self.watch(key, sub))
|
const state = yield* this.startCached(
|
||||||
return sub
|
Option.isSome(latestFinalState) && this.keyEquivalence(latestState.key, latestFinalState.value.key)
|
||||||
}).pipe(Effect.provide(this.context))
|
? latestFinalState.value
|
||||||
|
: {
|
||||||
|
key: latestState.key,
|
||||||
|
result: AsyncResult.initial(false),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* Effect.forkScoped(this.watch(state))
|
||||||
|
return state
|
||||||
|
}).pipe(
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
startCached(
|
startCached(
|
||||||
key: K,
|
previous: QueryState<K, A, E>,
|
||||||
initial: Result.Initial | Result.Final<A, E, P>,
|
): Effect.Effect<
|
||||||
): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, never, Scope.Scope | QueryClient.QueryClient | R> {
|
View.View<QueryState<K, A, E>>,
|
||||||
return Effect.flatMap(this.getCacheEntry(key), Option.match({
|
Cause.NoSuchElementError,
|
||||||
|
Scope.Scope | QueryClient.QueryClient | R
|
||||||
|
> {
|
||||||
|
return Effect.flatMap(this.getCacheEntry(previous.key), Option.match({
|
||||||
onSome: entry => Effect.flatMap(
|
onSome: entry => Effect.flatMap(
|
||||||
QueryClient.isQueryClientCacheEntryStale(entry),
|
QueryClient.isQueryClientCacheEntryStale(entry),
|
||||||
isStale => isStale
|
isStale => isStale
|
||||||
? this.start(key, Result.willRefresh(entry.result) as Result.Final<A, E, P>)
|
? this.start({
|
||||||
: Effect.succeed(Subscribable.make({
|
key: previous.key,
|
||||||
get: Effect.succeed(entry.result as Result.Result<A, E, P>),
|
result: entry.result as AsyncResult.AsyncResult<A, E>,
|
||||||
changes: Stream.make(entry.result as Result.Result<A, E, P>),
|
})
|
||||||
|
: Effect.succeed(View.make({
|
||||||
|
get: Effect.succeed({
|
||||||
|
key: previous.key,
|
||||||
|
result: entry.result as AsyncResult.AsyncResult<A, E>,
|
||||||
|
}),
|
||||||
|
get changes() {
|
||||||
|
return Stream.make({
|
||||||
|
key: previous.key,
|
||||||
|
result: entry.result as AsyncResult.AsyncResult<A, E>,
|
||||||
|
})
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
onNone: () => this.start(key, initial),
|
onNone: () => this.start(previous),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
start(
|
start(
|
||||||
key: K,
|
previous: QueryState<K, A, E>,
|
||||||
initial: Result.Initial | Result.Final<A, E, P>,
|
): Effect.Effect<
|
||||||
): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, never, Scope.Scope | R> {
|
View.View<QueryState<K, A, E>>,
|
||||||
const self = this
|
never,
|
||||||
return Effect.gen(function*() {
|
Scope.Scope | R
|
||||||
const [sub, fiber] = yield* Result.unsafeForkEffect<A, E, R, P>(
|
> {
|
||||||
Effect.onExit(self.f(key), () => Effect.flatMap(
|
return Effect.gen({ self: this }, function*() {
|
||||||
Effect.all([Effect.fiberId, SubscriptionRef.get(self.fiberRef)]),
|
const state = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(previous))
|
||||||
([currentFiberId, current]) => Option.match(current, {
|
|
||||||
onSome: value => Equal.equals(currentFiberId, value.id)
|
const fiber = yield* Effect.forkScoped(Effect.andThen(
|
||||||
? SubscriptionRef.set(self.fiberRef, Option.none())
|
Lens.update(state, previous => AsyncResult.match(previous.result, {
|
||||||
: Effect.succeed(undefined),
|
onInitial: () => ({
|
||||||
onNone: () => Effect.succeed(undefined),
|
key: previous.key,
|
||||||
|
result: AsyncResult.initial(true),
|
||||||
}),
|
}),
|
||||||
|
onSuccess: result => ({
|
||||||
|
key: previous.key,
|
||||||
|
result: AsyncResult.success(result.value, {
|
||||||
|
waiting: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
onFailure: result => ({
|
||||||
|
key: previous.key,
|
||||||
|
result: AsyncResult.failure(result.cause, {
|
||||||
|
waiting: true,
|
||||||
|
previousSuccess: result.previousSuccess,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
|
||||||
|
Effect.onExit(this.f(previous.key), exit => Lens.update(
|
||||||
|
state,
|
||||||
|
previous => Exit.match(exit, {
|
||||||
|
onSuccess: v => ({
|
||||||
|
key: previous.key,
|
||||||
|
result: AsyncResult.success(v),
|
||||||
|
}),
|
||||||
|
onFailure: c => AsyncResult.match(previous.result, {
|
||||||
|
onInitial: () => ({
|
||||||
|
key: previous.key,
|
||||||
|
result: AsyncResult.failure(c),
|
||||||
|
}),
|
||||||
|
onSuccess: v => ({
|
||||||
|
key: previous.key,
|
||||||
|
result: AsyncResult.failure(c, {
|
||||||
|
previousSuccess: Option.some(v),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
onFailure: v => ({
|
||||||
|
key: previous.key,
|
||||||
|
result: AsyncResult.failure(c, {
|
||||||
|
previousSuccess: v.previousSuccess,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
).pipe(
|
||||||
|
Effect.andThen(Effect.all([
|
||||||
|
Effect.fiberId,
|
||||||
|
Lens.get(this.fiber),
|
||||||
|
])),
|
||||||
|
Effect.flatMap(([fiberId, fiber]) => Option.match(fiber, {
|
||||||
|
onSome: v => fiberId === v.id
|
||||||
|
? Lens.set(this.fiber, Option.none())
|
||||||
|
: Effect.void,
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
})),
|
||||||
)),
|
)),
|
||||||
{ initial, initialProgress: self.initialProgress },
|
))
|
||||||
)
|
|
||||||
yield* SubscriptionRef.set(self.fiberRef, Option.some(fiber))
|
yield* Lens.set(this.fiber, Option.some(fiber))
|
||||||
return sub
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
key: K,
|
view: View.View<QueryState<K, A, E>>
|
||||||
sub: Subscribable.Subscribable<Result.Result<A, E, P>>,
|
): Effect.Effect<FinalQueryState<K, A, E>, never, QueryClient.QueryClient> {
|
||||||
): Effect.Effect<Result.Final<A, E, P>, never, QueryClient.QueryClient> {
|
return Effect.gen({ self: this }, function*() {
|
||||||
return Effect.flatMap(sub.get, initial => Stream.runFoldEffect(
|
const initial = yield* View.get(view)
|
||||||
Stream.takeUntil(sub.changes, result => Result.isFinal(result) && !Result.hasFlag(result)),
|
const final = yield* Stream.runFoldEffect(
|
||||||
|
View.changes(view),
|
||||||
() => initial,
|
() => initial,
|
||||||
(_, result) => Effect.as(SubscriptionRef.set(this.resultRef, result), result),
|
(_, state) => Effect.as(Lens.set(this.state, state), state),
|
||||||
) as Effect.Effect<Result.Final<A, E, P>>).pipe(
|
) as Effect.Effect<FinalQueryState<K, A, E>>
|
||||||
Effect.tap(result => SubscriptionRef.set(this.latestFinalResultRef, Option.some(result))),
|
|
||||||
Effect.tap(result => Result.isSuccess(result)
|
yield* Lens.set(this.latestFinalState, Option.some(final))
|
||||||
? Effect.asVoid(this.setCacheEntry(key, result))
|
if (AsyncResult.isSuccess(final.result))
|
||||||
: Effect.succeed(undefined)),
|
yield* this.setCacheEntry(final.key, final.result)
|
||||||
)
|
|
||||||
|
return final
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
makeCacheKey(key: K): QueryClient.QueryClientCacheKey {
|
makeCacheKey(key: K): QueryClient.QueryClientCacheKey {
|
||||||
return new QueryClient.QueryClientCacheKey(key, this.f as (key: Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>)
|
return new QueryClient.QueryClientCacheKey(key, this.f as (key: unknown) => Effect.Effect<unknown, unknown, unknown>)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCacheEntry(key: K): Effect.Effect<Option.Option<QueryClient.QueryClientCacheEntry>, never, QueryClient.QueryClient> {
|
getCacheEntry(
|
||||||
return Effect.flatMap(QueryClient.QueryClient, client => client.getCacheEntry(this.makeCacheKey(key)))
|
key: K
|
||||||
|
): Effect.Effect<Option.Option<QueryClient.QueryClientCacheEntry>, never, QueryClient.QueryClient> {
|
||||||
|
return Effect.andThen(
|
||||||
|
Effect.all([
|
||||||
|
Effect.succeed(this.makeCacheKey(key)),
|
||||||
|
QueryClient.QueryClient,
|
||||||
|
]),
|
||||||
|
([key, client]) => client.getCacheEntry(key),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setCacheEntry(key: K, result: Result.Success<A>): Effect.Effect<QueryClient.QueryClientCacheEntry, never, QueryClient.QueryClient> {
|
setCacheEntry(
|
||||||
return Effect.flatMap(QueryClient.QueryClient, client => client.setCacheEntry(this.makeCacheKey(key), result, this.staleTime))
|
key: K,
|
||||||
|
result: AsyncResult.Success<A, E>,
|
||||||
|
): Effect.Effect<QueryClient.QueryClientCacheEntry, never, QueryClient.QueryClient> {
|
||||||
|
return Effect.flatMap(
|
||||||
|
Effect.all([
|
||||||
|
Effect.succeed(this.makeCacheKey(key)),
|
||||||
|
QueryClient.QueryClient,
|
||||||
|
]),
|
||||||
|
([key, client]) => client.setCacheEntry(key, result, this.staleTime),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get invalidateCache(): Effect.Effect<void> {
|
get invalidateCache(): Effect.Effect<void> {
|
||||||
return Effect.flatMap(
|
return QueryClient.QueryClient.pipe(
|
||||||
QueryClient.QueryClient,
|
Effect.andThen(client => client.invalidateCacheEntries(this.f as (key: unknown) => Effect.Effect<unknown, unknown, unknown>)),
|
||||||
client => client.invalidateCacheEntries(this.f as (key: Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>),
|
Effect.provide(this.context),
|
||||||
).pipe(Effect.provide(this.context))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateCacheEntry(key: K): Effect.Effect<void> {
|
invalidateCacheEntry(key: K): Effect.Effect<void> {
|
||||||
return Effect.flatMap(
|
return Effect.all([
|
||||||
|
Effect.succeed(this.makeCacheKey(key)),
|
||||||
QueryClient.QueryClient,
|
QueryClient.QueryClient,
|
||||||
client => client.invalidateCacheEntry(this.makeCacheKey(key)),
|
]).pipe(
|
||||||
).pipe(Effect.provide(this.context))
|
Effect.andThen(([key, client]) => client.invalidateCacheEntry(key)),
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isQuery = (u: unknown): u is Query<readonly unknown[], unknown> => Predicate.hasProperty(u, QueryTypeId)
|
|
||||||
|
|
||||||
export declare namespace make {
|
export declare namespace make {
|
||||||
export interface Options<K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never> {
|
export interface Options<K, A, E = never, R = never> {
|
||||||
readonly key: Stream.Stream<K, KE, KR>
|
readonly key: View.View<K>,
|
||||||
readonly f: (key: NoInfer<K>) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
|
readonly keyEquivalence?: Equivalence.Equivalence<K>,
|
||||||
readonly initialProgress?: P
|
readonly f: (key: K) => Effect.Effect<A, E, R>
|
||||||
|
|
||||||
readonly staleTime?: Duration.Input
|
readonly staleTime?: Duration.Input
|
||||||
readonly refreshOnWindowFocus?: boolean
|
readonly refreshOnWindowFocus?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>(
|
export const make = Effect.fnUntraced(function* <K, A, E = never, R = never>(
|
||||||
options: make.Options<K, A, KE, KR, E, R, P>,
|
options: make.Options<K, A, E, R>
|
||||||
): Effect.fn.Return<
|
): Effect.fn.Return<
|
||||||
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<R, P>, P>,
|
Query<K, A, E, R>,
|
||||||
never,
|
Cause.NoSuchElementError,
|
||||||
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<R, P>
|
Scope.Scope | QueryClient.QueryClient | R
|
||||||
> {
|
> {
|
||||||
const client = yield* QueryClient.QueryClient
|
const client = yield* QueryClient.QueryClient
|
||||||
return new QueryImpl<K, A, KE, KR, E, Result.forkEffect.OutputContext<R, P>, P>(
|
|
||||||
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<R, P>>(),
|
return new QueryImpl(
|
||||||
|
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | R>(),
|
||||||
options.key,
|
options.key,
|
||||||
options.f as any,
|
options.keyEquivalence ?? Equal.asEquivalence(),
|
||||||
options.initialProgress as P,
|
options.f,
|
||||||
options.staleTime ?? client.defaultStaleTime,
|
|
||||||
|
options.staleTime ? yield* Effect.fromOption(Duration.fromInput(options.staleTime)) : client.defaultStaleTime,
|
||||||
options.refreshOnWindowFocus ?? client.defaultRefreshOnWindowFocus,
|
options.refreshOnWindowFocus ?? client.defaultRefreshOnWindowFocus,
|
||||||
yield* SubscriptionRef.make(Option.none<K>()),
|
|
||||||
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>())),
|
||||||
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
Lens.fromSubscriptionRef(yield* SubscriptionRef.make<QueryState<K, A, E>>({
|
||||||
yield* SubscriptionRef.make(Option.none<Result.Final<A, E, P>>()),
|
key: yield* View.get(options.key),
|
||||||
|
result: AsyncResult.initial(false),
|
||||||
|
})),
|
||||||
|
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<FinalQueryState<K, A, E>>())),
|
||||||
|
|
||||||
yield* Semaphore.make(1),
|
yield* Semaphore.make(1),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const service = <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>(
|
export const service = <K, A, E = never, R = never>(
|
||||||
options: make.Options<K, A, KE, KR, E, R, P>,
|
options: make.Options<K, A, E, R>
|
||||||
): Effect.Effect<
|
): Effect.Effect<
|
||||||
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<R, P>, P>,
|
Query<K, A, E, R>,
|
||||||
never,
|
Cause.NoSuchElementError,
|
||||||
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<R, P>
|
Scope.Scope | QueryClient.QueryClient | R
|
||||||
> => Effect.tap(make(options), query => Effect.asVoid(Effect.forkScoped(query.run)))
|
> => Effect.tap(
|
||||||
|
make(options),
|
||||||
const fromSubscriptionRef = <A>(ref: SubscriptionRef.SubscriptionRef<A>): Subscribable.Subscribable<A> => Subscribable.make({
|
query => Effect.forkScoped(query.run),
|
||||||
get: SubscriptionRef.get(ref),
|
)
|
||||||
changes: SubscriptionRef.changes(ref),
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,24 +1,7 @@
|
|||||||
import {
|
import { type Cause, Context, DateTime, Duration, Effect, Equal, Equivalence, Hash, HashMap, type Option, Pipeable, Predicate, Schedule, type Scope, Semaphore, SubscriptionRef } from "effect"
|
||||||
Context,
|
import type { AsyncResult } from "effect/unstable/reactivity"
|
||||||
DateTime,
|
import * as Lens from "./Lens.js"
|
||||||
Duration,
|
import type * as View from "./View.js"
|
||||||
Effect,
|
|
||||||
Equal,
|
|
||||||
Equivalence,
|
|
||||||
Hash,
|
|
||||||
HashMap,
|
|
||||||
Layer,
|
|
||||||
Option,
|
|
||||||
Pipeable,
|
|
||||||
Predicate,
|
|
||||||
Schedule,
|
|
||||||
Scope,
|
|
||||||
Semaphore,
|
|
||||||
SubscriptionRef,
|
|
||||||
} from "effect"
|
|
||||||
import { Subscribable } from "effect-lens"
|
|
||||||
import type * as Query from "./Query.js"
|
|
||||||
import type * as Result from "./Result.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const QueryClientServiceTypeId: unique symbol = Symbol.for("@effect-fc/QueryClient/QueryClientService")
|
export const QueryClientServiceTypeId: unique symbol = Symbol.for("@effect-fc/QueryClient/QueryClientService")
|
||||||
@@ -26,89 +9,87 @@ export type QueryClientServiceTypeId = typeof QueryClientServiceTypeId
|
|||||||
|
|
||||||
export interface QueryClientService extends Pipeable.Pipeable {
|
export interface QueryClientService extends Pipeable.Pipeable {
|
||||||
readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId
|
readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId
|
||||||
readonly cache: Subscribable.Subscribable<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>
|
|
||||||
readonly cacheGcTime: Duration.Input
|
readonly cache: View.View<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>
|
||||||
readonly defaultStaleTime: Duration.Input
|
readonly cacheGcTime: Duration.Duration
|
||||||
|
readonly defaultStaleTime: Duration.Duration
|
||||||
readonly defaultRefreshOnWindowFocus: boolean
|
readonly defaultRefreshOnWindowFocus: boolean
|
||||||
readonly run: Effect.Effect<void>
|
|
||||||
|
readonly run: Effect.Effect<void, Cause.NoSuchElementError>
|
||||||
getCacheEntry(key: QueryClientCacheKey): Effect.Effect<Option.Option<QueryClientCacheEntry>>
|
getCacheEntry(key: QueryClientCacheKey): Effect.Effect<Option.Option<QueryClientCacheEntry>>
|
||||||
setCacheEntry(key: QueryClientCacheKey, result: Result.Success<unknown>, staleTime: Duration.Input): Effect.Effect<QueryClientCacheEntry>
|
setCacheEntry(
|
||||||
invalidateCacheEntries(f: (key: Query.Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>): Effect.Effect<void>
|
key: QueryClientCacheKey,
|
||||||
|
result: AsyncResult.Success<unknown, unknown>,
|
||||||
|
staleTime: Duration.Duration,
|
||||||
|
): Effect.Effect<QueryClientCacheEntry>
|
||||||
|
invalidateCacheEntries(f: (key: unknown) => Effect.Effect<unknown, unknown, unknown>): Effect.Effect<void>
|
||||||
invalidateCacheEntry(key: QueryClientCacheKey): Effect.Effect<void>
|
invalidateCacheEntry(key: QueryClientCacheKey): Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueryClient extends Context.Service<QueryClient, QueryClientService>()(
|
export class QueryClient extends Context.Service<QueryClient, QueryClientService>()(
|
||||||
"@effect-fc/QueryClient/QueryClient",
|
"@effect-fc/QueryClient/QueryClient"
|
||||||
) {
|
) {}
|
||||||
static get Default(): Layer.Layer<QueryClient> {
|
|
||||||
return Layer.effect(QueryClient)(service())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QueryClientServiceImpl extends Pipeable.Class implements QueryClientService {
|
export class QueryClientServiceImpl
|
||||||
|
extends Pipeable.Class
|
||||||
|
implements QueryClientService {
|
||||||
readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId = QueryClientServiceTypeId
|
readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId = QueryClientServiceTypeId
|
||||||
readonly cache: Subscribable.Subscribable<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly cacheRef: SubscriptionRef.SubscriptionRef<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>,
|
readonly cache: Lens.Lens<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>,
|
||||||
readonly cacheGcTime: Duration.Input,
|
readonly cacheGcTime: Duration.Duration,
|
||||||
readonly defaultStaleTime: Duration.Input,
|
readonly defaultStaleTime: Duration.Duration,
|
||||||
readonly defaultRefreshOnWindowFocus: boolean,
|
readonly defaultRefreshOnWindowFocus: boolean,
|
||||||
readonly runSemaphore: Semaphore.Semaphore,
|
readonly runSemaphore: Semaphore.Semaphore,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.cache = Subscribable.make({
|
|
||||||
get: SubscriptionRef.get(cacheRef),
|
|
||||||
changes: SubscriptionRef.changes(cacheRef),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get run(): Effect.Effect<void> {
|
get run(): Effect.Effect<void, Cause.NoSuchElementError> {
|
||||||
return this.runSemaphore.withPermits(1)(Effect.repeat(
|
return this.runSemaphore.withPermits(1)(Effect.repeat(
|
||||||
Effect.flatMap(DateTime.now, now => SubscriptionRef.update(this.cacheRef, HashMap.filter(entry =>
|
Effect.flatMap(
|
||||||
|
DateTime.now,
|
||||||
|
now => Lens.update(this.cache, HashMap.filter(entry =>
|
||||||
Duration.isLessThan(
|
Duration.isLessThan(
|
||||||
DateTime.distance(entry.lastAccessedAt, now),
|
DateTime.distance(entry.lastAccessedAt, now),
|
||||||
Duration.sum(Duration.fromInputUnsafe(entry.staleTime), Duration.fromInputUnsafe(this.cacheGcTime)),
|
Duration.sum(entry.staleTime, this.cacheGcTime),
|
||||||
)
|
)
|
||||||
))),
|
)),
|
||||||
Schedule.spaced("30 seconds"),
|
),
|
||||||
|
Schedule.spaced("30 second"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
getCacheEntry(key: QueryClientCacheKey): Effect.Effect<Option.Option<QueryClientCacheEntry>> {
|
getCacheEntry(key: QueryClientCacheKey): Effect.Effect<Option.Option<QueryClientCacheEntry>> {
|
||||||
const self = this
|
return Effect.all([
|
||||||
return Effect.gen(function*() {
|
DateTime.now,
|
||||||
const entry = HashMap.get(yield* SubscriptionRef.get(self.cacheRef), key)
|
Effect.flatMap(
|
||||||
if (Option.isNone(entry)) return Option.none()
|
Effect.map(Lens.get(this.cache), HashMap.get(key)),
|
||||||
const now = yield* DateTime.now
|
Effect.fromOption,
|
||||||
const accessed = new QueryClientCacheEntry(
|
),
|
||||||
entry.value.result,
|
]).pipe(
|
||||||
entry.value.staleTime,
|
Effect.map(([now, entry]) => new QueryClientCacheEntry(entry.result, entry.staleTime, entry.createdAt, now)),
|
||||||
entry.value.createdAt,
|
Effect.tap(entry => Lens.update(this.cache, HashMap.set(key, entry))),
|
||||||
now,
|
Effect.option,
|
||||||
)
|
)
|
||||||
yield* SubscriptionRef.update(self.cacheRef, HashMap.set(key, accessed))
|
|
||||||
return Option.some(accessed)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setCacheEntry(
|
setCacheEntry(
|
||||||
key: QueryClientCacheKey,
|
key: QueryClientCacheKey,
|
||||||
result: Result.Success<unknown>,
|
result: AsyncResult.Success<unknown, unknown>,
|
||||||
staleTime: Duration.Input,
|
staleTime: Duration.Duration,
|
||||||
): Effect.Effect<QueryClientCacheEntry> {
|
): Effect.Effect<QueryClientCacheEntry> {
|
||||||
return Effect.flatMap(DateTime.now, now => {
|
return DateTime.now.pipe(
|
||||||
const entry = new QueryClientCacheEntry(result, staleTime, now, now)
|
Effect.map(now => new QueryClientCacheEntry(result, staleTime, now, now)),
|
||||||
return Effect.as(SubscriptionRef.update(this.cacheRef, HashMap.set(key, entry)), entry)
|
Effect.tap(entry => Lens.update(this.cache, HashMap.set(key, entry))),
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateCacheEntries(f: (key: Query.Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>): Effect.Effect<void> {
|
invalidateCacheEntries(f: (key: unknown) => Effect.Effect<unknown, unknown, unknown>): Effect.Effect<void> {
|
||||||
return SubscriptionRef.update(this.cacheRef, HashMap.filter((_, key) => !Equivalence.strictEqual()(key.f, f)))
|
return Lens.update(this.cache, HashMap.filter((_, key) => !Equivalence.strictEqual()(key.f, f)))
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateCacheEntry(key: QueryClientCacheKey): Effect.Effect<void> {
|
invalidateCacheEntry(key: QueryClientCacheKey): Effect.Effect<void> {
|
||||||
return SubscriptionRef.update(this.cacheRef, HashMap.remove(key))
|
return Lens.update(this.cache, HashMap.remove(key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,11 +103,13 @@ export declare namespace make {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const make = Effect.fnUntraced(function* (options: make.Options = {}): Effect.fn.Return<QueryClientService> {
|
export const make = Effect.fnUntraced(function* (
|
||||||
|
options: make.Options = {}
|
||||||
|
): Effect.fn.Return<QueryClientService, Cause.NoSuchElementError, never> {
|
||||||
return new QueryClientServiceImpl(
|
return new QueryClientServiceImpl(
|
||||||
yield* SubscriptionRef.make(HashMap.empty<QueryClientCacheKey, QueryClientCacheEntry>()),
|
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(HashMap.empty<QueryClientCacheKey, QueryClientCacheEntry>())),
|
||||||
options.cacheGcTime ?? "5 minutes",
|
yield* Effect.fromOption(Duration.fromInput(options.cacheGcTime ?? "5 minutes")),
|
||||||
options.defaultStaleTime ?? "0 minutes",
|
yield* Effect.fromOption(Duration.fromInput(options.defaultStaleTime ?? "0 minutes")),
|
||||||
options.defaultRefreshOnWindowFocus ?? true,
|
options.defaultRefreshOnWindowFocus ?? true,
|
||||||
yield* Semaphore.make(1),
|
yield* Semaphore.make(1),
|
||||||
)
|
)
|
||||||
@@ -136,46 +119,51 @@ export declare namespace service {
|
|||||||
export interface Options extends make.Options {}
|
export interface Options extends make.Options {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const service = (options?: service.Options): Effect.Effect<QueryClientService, never, Scope.Scope> => Effect.tap(
|
export const service = (
|
||||||
|
options?: service.Options
|
||||||
|
): Effect.Effect<QueryClientService, Cause.NoSuchElementError, Scope.Scope> => Effect.tap(
|
||||||
make(options),
|
make(options),
|
||||||
client => Effect.asVoid(Effect.forkScoped(client.run)),
|
client => Effect.forkScoped(client.run),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
export const QueryClientCacheKeyTypeId: unique symbol = Symbol.for("@effect-fc/QueryClient/QueryClientCacheKey")
|
export const QueryClientCacheKeyTypeId: unique symbol = Symbol.for("@effect-fc/QueryClient/QueryClientCacheKey")
|
||||||
export type QueryClientCacheKeyTypeId = typeof QueryClientCacheKeyTypeId
|
export type QueryClientCacheKeyTypeId = typeof QueryClientCacheKeyTypeId
|
||||||
|
|
||||||
export class QueryClientCacheKey extends Pipeable.Class implements Equal.Equal {
|
export class QueryClientCacheKey
|
||||||
|
extends Pipeable.Class
|
||||||
|
implements Pipeable.Pipeable, Equal.Equal {
|
||||||
readonly [QueryClientCacheKeyTypeId]: QueryClientCacheKeyTypeId = QueryClientCacheKeyTypeId
|
readonly [QueryClientCacheKeyTypeId]: QueryClientCacheKeyTypeId = QueryClientCacheKeyTypeId
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly key: Query.Query.AnyKey,
|
readonly key: unknown,
|
||||||
readonly f: (key: Query.Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>,
|
readonly f: (key: unknown) => Effect.Effect<unknown, unknown, unknown>,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
[Equal.symbol](that: Equal.Equal): boolean {
|
[Equal.symbol](that: Equal.Equal) {
|
||||||
return isQueryClientCacheKey(that)
|
return isQueryClientCacheKey(that) && Equal.equals(this.key, that.key) && Equivalence.strictEqual()(this.f, that.f)
|
||||||
&& Equivalence.Array(Equal.asEquivalence())(this.key, that.key)
|
|
||||||
&& Equivalence.strictEqual()(this.f, that.f)
|
|
||||||
}
|
}
|
||||||
|
[Hash.symbol]() {
|
||||||
[Hash.symbol](): number {
|
return Hash.combine(Hash.hash(this.f))(Hash.hash(this.key))
|
||||||
return Hash.combine(Hash.hash(this.f))(Hash.array(this.key))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isQueryClientCacheKey = (u: unknown): u is QueryClientCacheKey => Predicate.hasProperty(u, QueryClientCacheKeyTypeId)
|
export const isQueryClientCacheKey = (u: unknown): u is QueryClientCacheKey => Predicate.hasProperty(u, QueryClientCacheKeyTypeId)
|
||||||
|
|
||||||
|
|
||||||
export const QueryClientCacheEntryTypeId: unique symbol = Symbol.for("@effect-fc/QueryClient/QueryClientCacheEntry")
|
export const QueryClientCacheEntryTypeId: unique symbol = Symbol.for("@effect-fc/QueryClient/QueryClientCacheEntry")
|
||||||
export type QueryClientCacheEntryTypeId = typeof QueryClientCacheEntryTypeId
|
export type QueryClientCacheEntryTypeId = typeof QueryClientCacheEntryTypeId
|
||||||
|
|
||||||
export class QueryClientCacheEntry extends Pipeable.Class {
|
export class QueryClientCacheEntry
|
||||||
|
extends Pipeable.Class
|
||||||
|
implements Pipeable.Pipeable {
|
||||||
readonly [QueryClientCacheEntryTypeId]: QueryClientCacheEntryTypeId = QueryClientCacheEntryTypeId
|
readonly [QueryClientCacheEntryTypeId]: QueryClientCacheEntryTypeId = QueryClientCacheEntryTypeId
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly result: Result.Success<unknown>,
|
readonly result: AsyncResult.Success<unknown, unknown>,
|
||||||
readonly staleTime: Duration.Input,
|
readonly staleTime: Duration.Duration,
|
||||||
readonly createdAt: DateTime.DateTime,
|
readonly createdAt: DateTime.DateTime,
|
||||||
readonly lastAccessedAt: DateTime.DateTime,
|
readonly lastAccessedAt: DateTime.DateTime,
|
||||||
) {
|
) {
|
||||||
@@ -185,7 +173,9 @@ export class QueryClientCacheEntry extends Pipeable.Class {
|
|||||||
|
|
||||||
export const isQueryClientCacheEntry = (u: unknown): u is QueryClientCacheEntry => Predicate.hasProperty(u, QueryClientCacheEntryTypeId)
|
export const isQueryClientCacheEntry = (u: unknown): u is QueryClientCacheEntry => Predicate.hasProperty(u, QueryClientCacheEntryTypeId)
|
||||||
|
|
||||||
export const isQueryClientCacheEntryStale = (self: QueryClientCacheEntry): Effect.Effect<boolean> => Effect.map(
|
export const isQueryClientCacheEntryStale = (
|
||||||
|
self: QueryClientCacheEntry
|
||||||
|
): Effect.Effect<boolean, Cause.NoSuchElementError> => Effect.map(
|
||||||
DateTime.now,
|
DateTime.now,
|
||||||
now => Duration.isGreaterThanOrEqualTo(DateTime.distance(self.createdAt, now), Duration.fromInputUnsafe(self.staleTime)),
|
now => Duration.isGreaterThanOrEqualTo(DateTime.distance(self.createdAt, now), self.staleTime),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,41 +1,52 @@
|
|||||||
/** biome-ignore-all lint/complexity/useArrowFunction: React component names are intentional */
|
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||||
import { Context, Layer, ManagedRuntime, Predicate } from "effect"
|
import { type Context, Effect, Layer, ManagedRuntime, Predicate } from "effect"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import * as Component from "./Component.js"
|
||||||
import * as ErrorObserver from "./ErrorObserver.js"
|
import * as ErrorObserver from "./ErrorObserver.js"
|
||||||
import * as QueryClient from "./QueryClient.js"
|
import * as QueryClient from "./QueryClient.js"
|
||||||
|
|
||||||
|
|
||||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/ReactRuntime/ReactRuntime")
|
export const ReactRuntimeTypeId: unique symbol = Symbol.for("@effect-fc/ReactRuntime/ReactRuntime")
|
||||||
export type TypeId = typeof TypeId
|
export type ReactRuntimeTypeId = typeof ReactRuntimeTypeId
|
||||||
|
|
||||||
export interface ReactRuntime<R, ER> {
|
export interface ReactRuntime<R, ER> {
|
||||||
new(_: never): Record<string, never>
|
new(_: never): Record<string, never>
|
||||||
readonly [TypeId]: TypeId
|
readonly [ReactRuntimeTypeId]: ReactRuntimeTypeId
|
||||||
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
|
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
|
||||||
readonly context: React.Context<Context.Context<R>>
|
readonly context: React.Context<Context.Context<R>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReactRuntimeProto = Object.freeze({ [TypeId]: TypeId } as const)
|
const ReactRuntimeProto = Object.freeze({ [ReactRuntimeTypeId]: ReactRuntimeTypeId } as const)
|
||||||
|
|
||||||
export const Prelude: Layer.Layer<ErrorObserver.ErrorObserver | QueryClient.QueryClient> = Layer.merge(
|
export const preludeLayer: Layer.Layer<
|
||||||
|
| Component.ScopeMap
|
||||||
|
| ErrorObserver.ErrorObserver
|
||||||
|
| QueryClient.QueryClient
|
||||||
|
> = Layer.mergeAll(
|
||||||
|
Component.ScopeMap.layer,
|
||||||
ErrorObserver.layer,
|
ErrorObserver.layer,
|
||||||
QueryClient.QueryClient.Default,
|
QueryClient.QueryClient.Default,
|
||||||
)
|
)
|
||||||
|
|
||||||
export const isReactRuntime = (u: unknown): u is ReactRuntime<unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
|
||||||
|
export const isReactRuntime = (u: unknown): u is ReactRuntime<unknown, unknown> => Predicate.hasProperty(u, ReactRuntimeTypeId)
|
||||||
|
|
||||||
export const make = <R, ER>(
|
export const make = <R, ER>(
|
||||||
layer: Layer.Layer<R, ER>,
|
layer: Layer.Layer<R, ER>,
|
||||||
memoMap?: Layer.MemoMap,
|
memoMap?: Layer.MemoMap,
|
||||||
): ReactRuntime<R | ErrorObserver.ErrorObserver | QueryClient.QueryClient, ER> => Object.setPrototypeOf(
|
): ReactRuntime<Layer.Success<typeof preludeLayer> | R, ER> => Object.setPrototypeOf(
|
||||||
Object.assign(function() {}, {
|
Object.assign(function() {}, {
|
||||||
runtime: ManagedRuntime.make(Layer.merge(layer, Prelude), { memoMap }),
|
runtime: ManagedRuntime.make(
|
||||||
// biome-ignore lint/style/noNonNullAssertion: initialized by Provider before consumers render
|
Layer.merge(layer, preludeLayer),
|
||||||
context: React.createContext<Context.Context<R | ErrorObserver.ErrorObserver | QueryClient.QueryClient>>(null!),
|
{ memoMap },
|
||||||
|
),
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: context initialization
|
||||||
|
context: React.createContext<Context.Context<R>>(null!),
|
||||||
}),
|
}),
|
||||||
ReactRuntimeProto,
|
ReactRuntimeProto,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
export namespace Provider {
|
export namespace Provider {
|
||||||
export interface Props<R, ER> extends React.SuspenseProps {
|
export interface Props<R, ER> extends React.SuspenseProps {
|
||||||
readonly runtime: ReactRuntime<R, ER>
|
readonly runtime: ReactRuntime<R, ER>
|
||||||
@@ -44,8 +55,13 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Provider = <R, ER>(
|
export const Provider = <R, ER>(
|
||||||
{ runtime, children, ...suspenseProps }: Provider.Props<R, ER>,
|
{ runtime, children, ...suspenseProps }: Provider.Props<R, ER>
|
||||||
): React.ReactNode => {
|
): React.ReactNode => {
|
||||||
|
Effect.runSync(Component.useOnChange(
|
||||||
|
() => Effect.addFinalizer(() => runtime.runtime.disposeEffect),
|
||||||
|
[runtime],
|
||||||
|
))
|
||||||
|
|
||||||
const promise = React.useMemo(() => runtime.runtime.context(), [runtime])
|
const promise = React.useMemo(() => runtime.runtime.context(), [runtime])
|
||||||
|
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
@@ -60,12 +76,5 @@ const ProviderInner = <R, ER>(
|
|||||||
readonly runtime: ReactRuntime<R, ER>
|
readonly runtime: ReactRuntime<R, ER>
|
||||||
readonly promise: Promise<Context.Context<R>>
|
readonly promise: Promise<Context.Context<R>>
|
||||||
readonly children?: React.ReactNode
|
readonly children?: React.ReactNode
|
||||||
},
|
|
||||||
): React.ReactNode => {
|
|
||||||
const context = React.use(promise)
|
|
||||||
React.useEffect(() => () => {
|
|
||||||
void runtime.runtime.dispose()
|
|
||||||
}, [runtime])
|
|
||||||
|
|
||||||
return React.createElement(runtime.context, { value: context }, children)
|
|
||||||
}
|
}
|
||||||
|
): React.ReactNode => React.createElement(runtime.context, { value: React.use(promise) }, children)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Cause, Context, Data, Effect, Equal, Exit, type Fiber, Hash, Layer, Match, Pipeable, Predicate, pipe, type Scope, SubscriptionRef } from "effect"
|
import { Cause, Context, Data, Effect, Equal, Exit, type Fiber, Hash, Layer, Match, Pipeable, Predicate, pipe, type Scope, SubscriptionRef } from "effect"
|
||||||
import { Lens, Subscribable } from "effect-lens"
|
import { Lens } from "effect-lens"
|
||||||
|
import type { View } from "effect-lens/View"
|
||||||
|
|
||||||
|
|
||||||
export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
|
export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
|
||||||
@@ -207,7 +208,7 @@ export const unsafeForkEffect = Effect.fnUntraced(function* <A, E, R, P = never>
|
|||||||
effect: Effect.Effect<A, E, R>,
|
effect: Effect.Effect<A, E, R>,
|
||||||
options?: unsafeForkEffect.Options<NoInfer<A>, NoInfer<E>, P>,
|
options?: unsafeForkEffect.Options<NoInfer<A>, NoInfer<E>, P>,
|
||||||
): Effect.fn.Return<
|
): Effect.fn.Return<
|
||||||
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
readonly [result: View<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
||||||
never,
|
never,
|
||||||
Scope.Scope | unsafeForkEffect.OutputContext<R, P>
|
Scope.Scope | unsafeForkEffect.OutputContext<R, P>
|
||||||
> {
|
> {
|
||||||
@@ -242,7 +243,7 @@ export const forkEffect: {
|
|||||||
effect: Effect.Effect<A, E, forkEffect.InputContext<R, NoInfer<P>>>,
|
effect: Effect.Effect<A, E, forkEffect.InputContext<R, NoInfer<P>>>,
|
||||||
options?: forkEffect.Options<NoInfer<A>, NoInfer<E>, P>,
|
options?: forkEffect.Options<NoInfer<A>, NoInfer<E>, P>,
|
||||||
): Effect.Effect<
|
): Effect.Effect<
|
||||||
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
readonly [result: View<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
||||||
never,
|
never,
|
||||||
Scope.Scope | forkEffect.OutputContext<R, P>
|
Scope.Scope | forkEffect.OutputContext<R, P>
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import * as Form from "./Form.js"
|
|||||||
import * as Lens from "./Lens.js"
|
import * as Lens from "./Lens.js"
|
||||||
import * as Mutation from "./Mutation.js"
|
import * as Mutation from "./Mutation.js"
|
||||||
import * as Result from "./Result.js"
|
import * as Result from "./Result.js"
|
||||||
import * as Subscribable from "./Subscribable.js"
|
import * as View from "./View.js"
|
||||||
|
|
||||||
|
|
||||||
type FormSchema<A, I, R> = Schema.Top & {
|
type FormSchema<A, I, R> = Schema.Top & {
|
||||||
@@ -39,7 +39,7 @@ extends Form.Form<readonly [], A, I, never, never> {
|
|||||||
readonly [value: A, form: SubmittableForm<A, I, R, unknown, unknown, unknown>],
|
readonly [value: A, form: SubmittableForm<A, I, R, unknown, unknown, unknown>],
|
||||||
MA, ME, MR, MP
|
MA, ME, MR, MP
|
||||||
>
|
>
|
||||||
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, SchemaIssue.Issue>>, never, never>
|
readonly validationFiber: View.View<Option.Option<Fiber.Fiber<A, SchemaIssue.Issue>>, never, never>
|
||||||
readonly run: Effect.Effect<void>
|
readonly run: Effect.Effect<void>
|
||||||
readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementError>
|
readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementError>
|
||||||
}
|
}
|
||||||
@@ -50,9 +50,9 @@ extends Pipeable.Class implements SubmittableForm<A, I, R, MA, ME, MR, MP> {
|
|||||||
readonly [SubmittableFormTypeId]: SubmittableFormTypeId = SubmittableFormTypeId
|
readonly [SubmittableFormTypeId]: SubmittableFormTypeId = SubmittableFormTypeId
|
||||||
readonly path = [] as const
|
readonly path = [] as const
|
||||||
readonly encodedValue: Lens.Lens<I, never, never, never, never>
|
readonly encodedValue: Lens.Lens<I, never, never, never, never>
|
||||||
readonly isValidating: Subscribable.Subscribable<boolean, never, never>
|
readonly isValidating: View.View<boolean, never, never>
|
||||||
readonly canCommit: Subscribable.Subscribable<boolean, never, never>
|
readonly canCommit: View.View<boolean, never, never>
|
||||||
readonly isCommitting: Subscribable.Subscribable<boolean, never, never>
|
readonly isCommitting: View.View<boolean, never, never>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly schema: FormSchema<A, I, R>,
|
readonly schema: FormSchema<A, I, R>,
|
||||||
@@ -77,26 +77,26 @@ extends Pipeable.Class implements SubmittableForm<A, I, R, MA, ME, MR, MP> {
|
|||||||
),
|
),
|
||||||
lock: Lens.asLensImpl(internalEncodedValue).lock,
|
lock: Lens.asLensImpl(internalEncodedValue).lock,
|
||||||
})
|
})
|
||||||
this.isValidating = Subscribable.map(validationFiber, Option.isSome)
|
this.isValidating = View.map(validationFiber, Option.isSome)
|
||||||
const commitState = Subscribable.zipLatestAll(
|
const commitState = View.zipLatestAll(
|
||||||
value as any,
|
value as any,
|
||||||
issues as any,
|
issues as any,
|
||||||
validationFiber as any,
|
validationFiber as any,
|
||||||
mutation.result as any,
|
mutation.result as any,
|
||||||
) as unknown as Subscribable.Subscribable<readonly [
|
) as unknown as View.View<readonly [
|
||||||
Option.Option<A>,
|
Option.Option<A>,
|
||||||
readonly Form.FormIssue[],
|
readonly Form.FormIssue[],
|
||||||
Option.Option<Fiber.Fiber<A, SchemaIssue.Issue>>,
|
Option.Option<Fiber.Fiber<A, SchemaIssue.Issue>>,
|
||||||
Result.Result<MA, ME, MP>,
|
Result.Result<MA, ME, MP>,
|
||||||
]>
|
]>
|
||||||
this.canCommit = Subscribable.map(
|
this.canCommit = View.map(
|
||||||
commitState,
|
commitState,
|
||||||
([current, currentIssues, fiber, result]: readonly [Option.Option<A>, readonly Form.FormIssue[], Option.Option<Fiber.Fiber<A, SchemaIssue.Issue>>, Result.Result<MA, ME, MP>]) => Option.isSome(current)
|
([current, currentIssues, fiber, result]: readonly [Option.Option<A>, readonly Form.FormIssue[], Option.Option<Fiber.Fiber<A, SchemaIssue.Issue>>, Result.Result<MA, ME, MP>]) => Option.isSome(current)
|
||||||
&& currentIssues.length === 0
|
&& currentIssues.length === 0
|
||||||
&& Option.isNone(fiber)
|
&& Option.isNone(fiber)
|
||||||
&& !(Result.isRunning(result) || Result.hasRefreshingFlag(result)),
|
&& !(Result.isRunning(result) || Result.hasRefreshingFlag(result)),
|
||||||
)
|
)
|
||||||
this.isCommitting = Subscribable.map(
|
this.isCommitting = View.map(
|
||||||
mutation.result,
|
mutation.result,
|
||||||
result => Result.isRunning(result) || Result.hasRefreshingFlag(result),
|
result => Result.isRunning(result) || Result.hasRefreshingFlag(result),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import { Effect, Equivalence, Stream } from "effect"
|
|
||||||
import { Subscribable } from "effect-lens"
|
|
||||||
import * as React from "react"
|
|
||||||
import * as Component from "./Component.js"
|
|
||||||
|
|
||||||
|
|
||||||
export * from "effect-lens/Subscribable"
|
|
||||||
|
|
||||||
export const zipLatestAll = <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
|
||||||
...elements: T
|
|
||||||
): Subscribable.Subscribable<
|
|
||||||
[T[number]] extends [never]
|
|
||||||
? never
|
|
||||||
: { [K in keyof T]: T[K] extends Subscribable.Subscribable<infer A, infer _E, infer _R> ? A : never },
|
|
||||||
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer E, infer _R> ? E : never,
|
|
||||||
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer _E, infer R> ? R : never
|
|
||||||
> => Subscribable.make({
|
|
||||||
get: Effect.all(elements.map(v => v.get)),
|
|
||||||
changes: Stream.zipLatestAll(...elements.map(v => v.changes)),
|
|
||||||
}) as any
|
|
||||||
|
|
||||||
export declare namespace useAll {
|
|
||||||
export type Success<T extends readonly Subscribable.Subscribable<any, any, any>[]> = [T[number]] extends [never]
|
|
||||||
? never
|
|
||||||
: { [K in keyof T]: T[K] extends Subscribable.Subscribable<infer A, infer _E, infer _R> ? A : never }
|
|
||||||
|
|
||||||
export interface Options<A> {
|
|
||||||
readonly equivalence?: Equivalence.Equivalence<A>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAll = Effect.fnUntraced(function* <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
|
||||||
elements: T,
|
|
||||||
options?: useAll.Options<useAll.Success<NoInfer<T>>>,
|
|
||||||
): Effect.fn.Return<
|
|
||||||
useAll.Success<T>,
|
|
||||||
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer E, infer _R> ? E : never,
|
|
||||||
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer _E, infer R> ? R : never
|
|
||||||
> {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(
|
|
||||||
yield* Component.useOnMount(() => Effect.all(elements.map(v => v.get)))
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* Component.useReactEffect(() => Stream.zipLatestAll(...elements.map(ref => ref.changes)).pipe(
|
|
||||||
Stream.changesWith((options?.equivalence as Equivalence.Equivalence<any[]> | undefined) ?? Equivalence.Array(Equivalence.strictEqual())),
|
|
||||||
Stream.runForEach(v =>
|
|
||||||
Effect.sync(() => setReactStateValue(v))
|
|
||||||
),
|
|
||||||
Effect.forkScoped,
|
|
||||||
), elements)
|
|
||||||
|
|
||||||
return reactStateValue as any
|
|
||||||
})
|
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from "effect"
|
} from "effect"
|
||||||
import * as Form from "./Form.js"
|
import * as Form from "./Form.js"
|
||||||
import * as Lens from "./Lens.js"
|
import * as Lens from "./Lens.js"
|
||||||
import * as Subscribable from "./Subscribable.js"
|
import * as View from "./View.js"
|
||||||
|
|
||||||
|
|
||||||
type FormSchema<A, I, R> = Schema.Top & {
|
type FormSchema<A, I, R> = Schema.Top & {
|
||||||
@@ -43,7 +43,7 @@ export interface SynchronizedForm<
|
|||||||
readonly schema: FormSchema<A, I, R>
|
readonly schema: FormSchema<A, I, R>
|
||||||
readonly context: Context.Context<Scope.Scope | R | TRR | TRW>
|
readonly context: Context.Context<Scope.Scope | R | TRR | TRW>
|
||||||
readonly target: Lens.Lens<A, TER, TEW, TRR, TRW>
|
readonly target: Lens.Lens<A, TER, TEW, TRR, TRW>
|
||||||
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, SchemaIssue.Issue>>, never, never>
|
readonly validationFiber: View.View<Option.Option<Fiber.Fiber<A, SchemaIssue.Issue>>, never, never>
|
||||||
readonly run: Effect.Effect<void, TER>
|
readonly run: Effect.Effect<void, TER>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,10 +59,10 @@ export class SynchronizedFormImpl<
|
|||||||
readonly [Form.FormTypeId]: Form.FormTypeId = Form.FormTypeId
|
readonly [Form.FormTypeId]: Form.FormTypeId = Form.FormTypeId
|
||||||
readonly [SynchronizedFormTypeId]: SynchronizedFormTypeId = SynchronizedFormTypeId
|
readonly [SynchronizedFormTypeId]: SynchronizedFormTypeId = SynchronizedFormTypeId
|
||||||
readonly path = [] as const
|
readonly path = [] as const
|
||||||
readonly value: Subscribable.Subscribable<Option.Option<A>, TER, never>
|
readonly value: View.View<Option.Option<A>, TER, never>
|
||||||
readonly encodedValue: Lens.Lens<I, TER, TER | TEW, never, never>
|
readonly encodedValue: Lens.Lens<I, TER, TER | TEW, never, never>
|
||||||
readonly isValidating: Subscribable.Subscribable<boolean, never, never>
|
readonly isValidating: View.View<boolean, never, never>
|
||||||
readonly canCommit: Subscribable.Subscribable<boolean, never, never>
|
readonly canCommit: View.View<boolean, never, never>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly schema: FormSchema<A, I, R>,
|
readonly schema: FormSchema<A, I, R>,
|
||||||
@@ -75,7 +75,7 @@ export class SynchronizedFormImpl<
|
|||||||
readonly runSemaphore: Semaphore.Semaphore,
|
readonly runSemaphore: Semaphore.Semaphore,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.value = Subscribable.make({
|
this.value = View.make({
|
||||||
get: Effect.provide(Effect.map(target.get, Option.some), context),
|
get: Effect.provide(Effect.map(target.get, Option.some), context),
|
||||||
changes: Stream.provideContext(
|
changes: Stream.provideContext(
|
||||||
target.changes.pipe(
|
target.changes.pipe(
|
||||||
@@ -94,13 +94,13 @@ export class SynchronizedFormImpl<
|
|||||||
),
|
),
|
||||||
lock: Lens.asLensImpl(internalEncodedValue).lock,
|
lock: Lens.asLensImpl(internalEncodedValue).lock,
|
||||||
}) as unknown as Lens.Lens<I, TER, TER | TEW, never, never>
|
}) as unknown as Lens.Lens<I, TER, TER | TEW, never, never>
|
||||||
this.isValidating = Subscribable.map(validationFiber, Option.isSome)
|
this.isValidating = View.map(validationFiber, Option.isSome)
|
||||||
const commitState = Subscribable.zipLatestAll(issues as any, validationFiber as any, isCommitting as any) as unknown as Subscribable.Subscribable<readonly [
|
const commitState = View.zipLatestAll(issues as any, validationFiber as any, isCommitting as any) as unknown as View.View<readonly [
|
||||||
readonly Form.FormIssue[],
|
readonly Form.FormIssue[],
|
||||||
Option.Option<Fiber.Fiber<A, SchemaIssue.Issue>>,
|
Option.Option<Fiber.Fiber<A, SchemaIssue.Issue>>,
|
||||||
boolean,
|
boolean,
|
||||||
]>
|
]>
|
||||||
this.canCommit = Subscribable.map(
|
this.canCommit = View.map(
|
||||||
commitState,
|
commitState,
|
||||||
([currentIssues, fiber, committing]) => currentIssues.length === 0 && Option.isNone(fiber) && !committing,
|
([currentIssues, fiber, committing]) => currentIssues.length === 0 && Option.isNone(fiber) && !committing,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Effect, Equivalence, Stream } from "effect"
|
||||||
|
import { View } from "effect-lens"
|
||||||
|
import * as React from "react"
|
||||||
|
import * as Component from "./Component.js"
|
||||||
|
|
||||||
|
|
||||||
|
export * from "effect-lens/View"
|
||||||
|
|
||||||
|
export declare namespace useAll {
|
||||||
|
export type Success<T extends readonly View.View<any, any, any>[]> = [T[number]] extends [never]
|
||||||
|
? never
|
||||||
|
: { [K in keyof T]: T[K] extends View.View<infer A, infer _E, infer _R> ? A : never }
|
||||||
|
|
||||||
|
export interface Options<A> {
|
||||||
|
readonly equivalence?: Equivalence.Equivalence<A>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAll = Effect.fnUntraced(function* <const T extends readonly View.View<any, any, any>[]>(
|
||||||
|
elements: T,
|
||||||
|
options?: useAll.Options<useAll.Success<NoInfer<T>>>,
|
||||||
|
): Effect.fn.Return<
|
||||||
|
useAll.Success<T>,
|
||||||
|
[T[number]] extends [never] ? never : T[number] extends View.View<infer _A, infer E, infer _R> ? E : never,
|
||||||
|
[T[number]] extends [never] ? never : T[number] extends View.View<infer _A, infer _E, infer R> ? R : never
|
||||||
|
> {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(
|
||||||
|
yield* Component.useOnMount(() => Effect.all(elements.map(View.get)))
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* Component.useReactEffect(() => View.changes(View.zipLatestAll(...elements)).pipe(
|
||||||
|
Stream.changesWith((options?.equivalence as Equivalence.Equivalence<any[]> | undefined) ?? Equivalence.Array(Equivalence.strictEqual())),
|
||||||
|
Stream.runForEach(v =>
|
||||||
|
Effect.sync(() => setReactStateValue(v))
|
||||||
|
),
|
||||||
|
Effect.forkScoped,
|
||||||
|
), elements)
|
||||||
|
|
||||||
|
return reactStateValue as any
|
||||||
|
})
|
||||||
@@ -13,5 +13,5 @@ export * as Result from "./Result.js"
|
|||||||
export * as SetStateAction from "./SetStateAction.js"
|
export * as SetStateAction from "./SetStateAction.js"
|
||||||
export * as Stream from "./Stream.js"
|
export * as Stream from "./Stream.js"
|
||||||
export * as SubmittableForm from "./SubmittableForm.js"
|
export * as SubmittableForm from "./SubmittableForm.js"
|
||||||
export * as Subscribable from "./Subscribable.js"
|
|
||||||
export * as SynchronizedForm from "./SynchronizedForm.js"
|
export * as SynchronizedForm from "./SynchronizedForm.js"
|
||||||
|
export * as View from "./View.js"
|
||||||
|
|||||||
+7
-42
@@ -1,10 +1,10 @@
|
|||||||
import { render, screen, waitFor } from "@testing-library/react"
|
import { render, screen, waitFor } from "@testing-library/react"
|
||||||
import { Effect, Fiber, Layer, Stream, SubscriptionRef } from "effect"
|
import { Effect, Layer, SubscriptionRef } from "effect"
|
||||||
import { Lens } from "effect-lens"
|
import { Lens } from "effect-lens"
|
||||||
import { describe, expect, it } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
import * as Component from "../src/Component.js"
|
import * as Component from "../src/Component.js"
|
||||||
import * as ReactRuntime from "../src/ReactRuntime.js"
|
import * as ReactRuntime from "../src/ReactRuntime.js"
|
||||||
import * as Subscribable from "../src/Subscribable.js"
|
import * as View from "../src/View.js"
|
||||||
|
|
||||||
|
|
||||||
const makeRuntime = async () => {
|
const makeRuntime = async () => {
|
||||||
@@ -18,42 +18,7 @@ const makeRuntime = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Subscribable", () => {
|
describe("View", () => {
|
||||||
it("zipLatestAll reads current values from all inputs", async () => {
|
|
||||||
const leftRef = await Effect.runPromise(SubscriptionRef.make(1))
|
|
||||||
const rightRef = await Effect.runPromise(SubscriptionRef.make("a"))
|
|
||||||
const left = Lens.fromSubscriptionRef(leftRef)
|
|
||||||
const right = Lens.fromSubscriptionRef(rightRef)
|
|
||||||
|
|
||||||
const zipped = Subscribable.zipLatestAll(left, right)
|
|
||||||
|
|
||||||
expect(await Effect.runPromise(zipped.get)).toEqual([1, "a"])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("zipLatestAll emits updates when any input changes", async () => {
|
|
||||||
const leftRef = await Effect.runPromise(SubscriptionRef.make(1))
|
|
||||||
const rightRef = await Effect.runPromise(SubscriptionRef.make("a"))
|
|
||||||
const left = Lens.fromSubscriptionRef(leftRef)
|
|
||||||
const right = Lens.fromSubscriptionRef(rightRef)
|
|
||||||
|
|
||||||
const zipped = Subscribable.zipLatestAll(left, right)
|
|
||||||
const values: Array<readonly [number, string]> = []
|
|
||||||
|
|
||||||
const collector = Effect.runFork(Effect.scoped(zipped.changes.pipe(
|
|
||||||
Stream.runForEach(value => Effect.sync(() => {
|
|
||||||
values.push(value as readonly [number, string])
|
|
||||||
})),
|
|
||||||
)))
|
|
||||||
|
|
||||||
await Effect.runPromise(Lens.set(left, 2))
|
|
||||||
await waitFor(() => expect(values).toContainEqual([2, "a"]))
|
|
||||||
|
|
||||||
await Effect.runPromise(Lens.set(right, "b"))
|
|
||||||
await waitFor(() => expect(values).toContainEqual([2, "b"]))
|
|
||||||
|
|
||||||
await Effect.runPromise(Fiber.interrupt(collector))
|
|
||||||
})
|
|
||||||
|
|
||||||
it("useAll returns the latest values and rerenders when any input changes", async () => {
|
it("useAll returns the latest values and rerenders when any input changes", async () => {
|
||||||
const { runtime, effectRuntime, dispose } = await makeRuntime()
|
const { runtime, effectRuntime, dispose } = await makeRuntime()
|
||||||
const countRef = await Effect.runPromise(SubscriptionRef.make(1))
|
const countRef = await Effect.runPromise(SubscriptionRef.make(1))
|
||||||
@@ -61,8 +26,8 @@ describe("Subscribable", () => {
|
|||||||
const count = Lens.fromSubscriptionRef(countRef)
|
const count = Lens.fromSubscriptionRef(countRef)
|
||||||
const label = Lens.fromSubscriptionRef(labelRef)
|
const label = Lens.fromSubscriptionRef(labelRef)
|
||||||
|
|
||||||
const Probe = Component.makeUntraced("SubscribableUseAllProbe")(function*() {
|
const Probe = Component.makeUntraced("ViewUseAllProbe")(function*() {
|
||||||
const [currentCount, currentLabel] = yield* Subscribable.useAll([count, label])
|
const [currentCount, currentLabel] = yield* View.useAll([count, label])
|
||||||
|
|
||||||
return <div>{`${currentCount}:${currentLabel}`}</div>
|
return <div>{`${currentCount}:${currentLabel}`}</div>
|
||||||
}).pipe(
|
}).pipe(
|
||||||
@@ -94,8 +59,8 @@ describe("Subscribable", () => {
|
|||||||
const item = Lens.fromSubscriptionRef(itemRef)
|
const item = Lens.fromSubscriptionRef(itemRef)
|
||||||
const flag = Lens.fromSubscriptionRef(flagRef)
|
const flag = Lens.fromSubscriptionRef(flagRef)
|
||||||
|
|
||||||
const Probe = Component.makeUntraced("SubscribableUseAllEquivalenceProbe")(function*() {
|
const Probe = Component.makeUntraced("ViewUseAllEquivalenceProbe")(function*() {
|
||||||
const [currentItem, currentFlag] = yield* Subscribable.useAll([item, flag], {
|
const [currentItem, currentFlag] = yield* View.useAll([item, flag], {
|
||||||
equivalence: ([selfItem, selfFlag], [thatItem, thatFlag]) =>
|
equivalence: ([selfItem, selfFlag], [thatItem, thatFlag]) =>
|
||||||
selfItem.id === thatItem.id && selfFlag === thatFlag,
|
selfItem.id === thatItem.id && selfFlag === thatFlag,
|
||||||
})
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Effect, Layer, SubscriptionRef } from "effect"
|
import { Effect, Layer, SubscriptionRef } from "effect"
|
||||||
import { Component, Lens, ReactRuntime, Subscribable } from "effect-fc-next"
|
import { Component, Lens, ReactRuntime, View } from "effect-fc-next"
|
||||||
import { StrictMode } from "react"
|
import { StrictMode } from "react"
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
@@ -10,7 +10,7 @@ const Counter = Component.make("Counter")(function*() {
|
|||||||
SubscriptionRef.make(0),
|
SubscriptionRef.make(0),
|
||||||
Lens.fromSubscriptionRef,
|
Lens.fromSubscriptionRef,
|
||||||
))
|
))
|
||||||
const [value] = yield* Subscribable.useAll([count])
|
const [value] = yield* View.useAll([count])
|
||||||
const increment = yield* Component.useCallbackSync(
|
const increment = yield* Component.useCallbackSync(
|
||||||
() => Lens.update(count, n => n + 1),
|
() => Lens.update(count, n => n + 1),
|
||||||
[count],
|
[count],
|
||||||
|
|||||||
Reference in New Issue
Block a user