diff --git a/packages/effect-fc/src/Mutation.ts b/packages/effect-fc/src/Mutation.ts new file mode 100644 index 0000000..69de1a4 --- /dev/null +++ b/packages/effect-fc/src/Mutation.ts @@ -0,0 +1,124 @@ +import { type Context, Effect, Equal, type Fiber, Option, Pipeable, Predicate, type Scope, Stream, type Subscribable, SubscriptionRef } from "effect" +import * as Result from "./Result.js" + + +export const MutationTypeId: unique symbol = Symbol.for("@effect-fc/Mutation/Mutation") +export type MutationTypeId = typeof MutationTypeId + +export interface Mutation +extends Pipeable.Pipeable { + readonly [MutationTypeId]: MutationTypeId + + readonly context: Context.Context + readonly f: (key: K) => Effect.Effect + readonly initialProgress: P + + readonly latestKey: Subscribable.Subscribable> + readonly fiber: Subscribable.Subscribable>> + readonly result: Subscribable.Subscribable> + + mutate(key: K): Effect.Effect> + mutateSubscribable(key: K): Effect.Effect>> +} + +export class MutationImpl +extends Pipeable.Class() implements Mutation { + readonly [MutationTypeId]: MutationTypeId = MutationTypeId + + constructor( + readonly context: Context.Context>, + readonly f: (key: K) => Effect.Effect, + readonly initialProgress: P, + + readonly latestKey: SubscriptionRef.SubscriptionRef>, + readonly fiber: SubscriptionRef.SubscriptionRef>>, + readonly result: SubscriptionRef.SubscriptionRef>, + ) { + super() + } + + mutate(key: K): Effect.Effect> { + return SubscriptionRef.set(this.latestKey, Option.some(key)).pipe( + Effect.andThen(Effect.provide(this.start(key), this.context)), + Effect.andThen(sub => this.watch(sub)), + ) + } + + mutateSubscribable(key: K): Effect.Effect>> { + return Effect.andThen( + SubscriptionRef.set(this.latestKey, Option.some(key)), + Effect.provide(this.start(key), this.context) + ) + } + + start(key: K): Effect.Effect< + Subscribable.Subscribable>, + never, + Scope.Scope | R + > { + return this.result.pipe( + Effect.map(previous => (Result.isSuccess(previous) || Result.isFailure(previous)) + ? previous + : undefined + ), + Effect.andThen(previous => Result.unsafeForkEffect( + Effect.onExit(this.f(key), () => Effect.andThen( + Effect.all([Effect.fiberId, this.fiber]), + ([currentFiberId, fiber]) => Option.match(fiber, { + onSome: v => Equal.equals(currentFiberId, v.id()) + ? SubscriptionRef.set(this.fiber, Option.none()) + : Effect.void, + onNone: () => Effect.void, + }) + )), + + { + initialProgress: this.initialProgress, + previous, + } as Result.unsafeForkEffect.Options, + )), + Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))), + Effect.map(([sub]) => sub), + ) + } + + watch( + sub: Subscribable.Subscribable> + ): Effect.Effect> { + return Effect.andThen( + sub.get, + initial => Stream.runFoldEffect( + Stream.filter(sub.changes, Predicate.not(Result.isInitial)), + initial, + (_, result) => Effect.as(SubscriptionRef.set(this.result, result), result), + ), + ) + } +} + +export const isMutation = (u: unknown): u is Mutation => Predicate.hasProperty(u, MutationTypeId) + +export declare namespace make { + export interface Options { + readonly f: (key: NoInfer) => Effect.Effect>> + readonly initialProgress?: P + } +} + +export const make = Effect.fnUntraced(function* ( + options: make.Options +): Effect.fn.Return< + Mutation, P>, + never, + Scope.Scope | Result.forkEffect.OutputContext +> { + return new MutationImpl( + yield* Effect.context>(), + options.f as any, + options.initialProgress as P, + + yield* SubscriptionRef.make(Option.none()), + yield* SubscriptionRef.make(Option.none>()), + yield* SubscriptionRef.make(Result.initial()), + ) +}) diff --git a/packages/effect-fc/src/Query.ts b/packages/effect-fc/src/Query.ts index 159198e..0ad366e 100644 --- a/packages/effect-fc/src/Query.ts +++ b/packages/effect-fc/src/Query.ts @@ -5,10 +5,11 @@ import * as Result from "./Result.js" export const QueryTypeId: unique symbol = Symbol.for("@effect-fc/Query/Query") export type QueryTypeId = typeof QueryTypeId -export interface Query +export interface Query extends Pipeable.Pipeable { readonly [QueryTypeId]: QueryTypeId + readonly context: Context.Context readonly key: Stream.Stream readonly f: (key: K) => Effect.Effect readonly initialProgress: P @@ -22,11 +23,12 @@ extends Pipeable.Pipeable { readonly refresh: Effect.Effect, Cause.NoSuchElementException> } -class QueryImpl +export class QueryImpl extends Pipeable.Class() implements Query { readonly [QueryTypeId]: QueryTypeId = QueryTypeId constructor( + readonly context: Context.Context>, readonly key: Stream.Stream, readonly f: (key: K) => Effect.Effect, readonly initialProgress: P, @@ -34,8 +36,6 @@ extends Pipeable.Class() implements Query { readonly latestKey: SubscriptionRef.SubscriptionRef>, readonly fiber: SubscriptionRef.SubscriptionRef>>, readonly result: SubscriptionRef.SubscriptionRef>, - - readonly context: Context.Context>, ) { super() } @@ -83,15 +83,15 @@ extends Pipeable.Class() implements Query { > { return this.result.pipe( Effect.map(previous => (Result.isSuccess(previous) || Result.isFailure(previous)) - ? Option.some(previous) - : Option.none() + ? previous + : undefined ), Effect.andThen(previous => Result.unsafeForkEffect( Effect.onExit(this.f(key), () => SubscriptionRef.set(this.fiber, Option.none())), { initialProgress: this.initialProgress, - refresh: refresh && Option.isSome(previous), - previous: Option.getOrUndefined(previous), + refresh: refresh && previous, + previous, } as Result.unsafeForkEffect.Options, )), Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))), @@ -131,6 +131,7 @@ export const make = Effect.fnUntraced(function* > { return new QueryImpl( + yield* Effect.context>(), options.key, options.f as any, options.initialProgress as P, @@ -138,8 +139,6 @@ export const make = Effect.fnUntraced(function* ()), yield* SubscriptionRef.make(Option.none>()), yield* SubscriptionRef.make(Result.initial()), - - yield* Effect.context>(), ) }) diff --git a/packages/effect-fc/src/index.ts b/packages/effect-fc/src/index.ts index 18468a4..2167ff9 100644 --- a/packages/effect-fc/src/index.ts +++ b/packages/effect-fc/src/index.ts @@ -3,6 +3,7 @@ export * as Component from "./Component.js" export * as ErrorObserver from "./ErrorObserver.js" export * as Form from "./Form.js" export * as Memoized from "./Memoized.js" +export * as Mutation from "./Mutation.js" export * as PropertyPath from "./PropertyPath.js" export * as PubSub from "./PubSub.js" export * as Query from "./Query.js"