Add Effect v4 support, Fast Refresh tooling, and revamped docs #56

Merged
Thilawyn merged 133 commits from next into master 2026-07-26 02:32:59 +02:00
2 changed files with 90 additions and 79 deletions
Showing only changes of commit e5ac120cea - Show all commits
+89 -78
View File
@@ -1,146 +1,157 @@
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 type * as Subscribable from "./Subscribable.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 extends Mutation.AnyKey, in out A, in 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: Subscribable.Subscribable<Option.Option<K>>
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>> readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>> readonly result: Subscribable.Subscribable<AsyncResult.AsyncResult<A, E>>
readonly latestFinalResult: Subscribable.Subscribable<Option.Option<Result.Final<A, E, P>>> readonly latestFinalResult: Subscribable.Subscribable<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>>> mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<AsyncResult.AsyncResult<A, E>>>
} }
export declare namespace Mutation { export declare namespace Mutation {
export type AnyKey = readonly any[] 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> export class MutationImpl<in out K extends Mutation.AnyKey, in out A, in out E = never, in out R = never>
extends Pipeable.Class implements Mutation<K, A, E, R, P> { 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 result: 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>>> { mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<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>>, Subscribable.Subscribable<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: Subscribable.Subscribable<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 state.get.pipe(
Effect.andThen(initial => Stream.runFoldEffect( Effect.andThen(initial => Stream.runFoldEffect(
Stream.takeUntil(sub.changes, result => Result.isFinal(result) && !Result.hasFlag(result)), state.changes,
() => initial, () => initial,
(_, result) => Effect.as(SubscriptionRef.set(this.resultRef, result), result), (_, result) => Effect.as(Lens.set(this.result, 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 const isMutation = (u: unknown): u is Mutation<readonly 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 extends Mutation.AnyKey = 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* <const K extends Mutation.AnyKey = 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 -1
View File
@@ -286,7 +286,7 @@ extends Pipeable.Class implements Query<K, A, KE, KR, E, R> {
} }
export const isQuery = (u: unknown): u is Query<readonly unknown[], unknown> => Predicate.hasProperty(u, QueryTypeId) export const isQuery = (u: unknown): u is Query<readonly unknown[], unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, QueryTypeId)
export declare namespace make { export declare namespace make {