diff --git a/bun.lock b/bun.lock index 69dcdf1..91d7915 100644 --- a/bun.lock +++ b/bun.lock @@ -58,17 +58,18 @@ "name": "effect-fc-next", "version": "0.1.0-beta.0", "dependencies": { - "effect-lens": "2.0.0-beta.0", + "effect-lens": "^2.0.0-beta.1", }, "devDependencies": { - "@effect/platform-browser": "^4.0.0-beta.85", + "@effect/platform-browser": "4.0.0-beta.98", "@testing-library/react": "^16.3.0", + "effect": "4.0.0-beta.98", "jsdom": "^26.1.0", "vitest": "^3.2.4", }, "peerDependencies": { "@types/react": "^19.2.0", - "effect": "4.0.0-beta.85", + "effect": "4.0.0-beta.98", "react": "^19.2.0", }, }, @@ -3063,11 +3064,11 @@ "dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], - "effect-fc-next/@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.87", "", { "dependencies": { "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^4.0.0-beta.87" } }, "sha512-sBUUdRt5PjFPLI7+ayxEX/jgnjCbOuc3I//98CmqiwhfgWbCMZ/rofYYb8Zk5tLpNsmdHnWgHWTsBLvH2SFrCw=="], + "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": ["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": ["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.0", "", { "peerDependencies": { "effect": "4.0.0-beta.85" } }, "sha512-ntsRQSrzoX+GsNpkNVEGqebl/g1YOEDPIFRAHigf+4P6AtGnnOFcklqDHLrbm4zqnaxuWEgQvvn1J6S3eyC4yw=="], + "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=="], @@ -3373,10 +3374,16 @@ "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/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=="], "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=="], diff --git a/packages/effect-fc-next/package.json b/packages/effect-fc-next/package.json index fa29dbf..2375e0e 100644 --- a/packages/effect-fc-next/package.json +++ b/packages/effect-fc-next/package.json @@ -39,17 +39,18 @@ "clean:modules": "rm -rf node_modules" }, "devDependencies": { - "@effect/platform-browser": "^4.0.0-beta.85", + "@effect/platform-browser": "4.0.0-beta.98", "@testing-library/react": "^16.3.0", + "effect": "4.0.0-beta.98", "jsdom": "^26.1.0", "vitest": "^3.2.4" }, "peerDependencies": { "@types/react": "^19.2.0", - "effect": "4.0.0-beta.85", + "effect": "4.0.0-beta.98", "react": "^19.2.0" }, "dependencies": { - "effect-lens": "2.0.0-beta.0" + "effect-lens": "^2.0.0-beta.1" } } diff --git a/packages/effect-fc-next/src/Form.ts b/packages/effect-fc-next/src/Form.ts index 30039aa..e3ce5ea 100644 --- a/packages/effect-fc-next/src/Form.ts +++ b/packages/effect-fc-next/src/Form.ts @@ -2,7 +2,7 @@ import { Array, type Cause, Chunk, type Duration, Effect, Equal, Function, ident import type * as React from "react" import * as Component from "./Component.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") @@ -18,12 +18,12 @@ extends Pipeable.Pipeable { readonly [FormTypeId]: FormTypeId readonly path: P - readonly value: Subscribable.Subscribable, ER, never> + readonly value: View.View, ER, never> readonly encodedValue: Lens.Lens - readonly issues: Subscribable.Subscribable - readonly isValidating: Subscribable.Subscribable - readonly canCommit: Subscribable.Subscribable - readonly isCommitting: Subscribable.Subscribable + readonly issues: View.View + readonly isValidating: View.View + readonly canCommit: View.View + readonly isCommitting: View.View } export class FormImpl @@ -32,12 +32,12 @@ extends Pipeable.Class implements Form { constructor( readonly path: P, - readonly value: Subscribable.Subscribable, ER, never>, + readonly value: View.View, ER, never>, readonly encodedValue: Lens.Lens, - readonly issues: Subscribable.Subscribable, - readonly isValidating: Subscribable.Subscribable, - readonly canCommit: Subscribable.Subscribable, - readonly isCommitting: Subscribable.Subscribable, + readonly issues: View.View, + readonly isValidating: View.View, + readonly canCommit: View.View, + readonly isCommitting: View.View, ) { super() } @@ -71,9 +71,9 @@ export const focusObjectOn: { return new FormImpl( path, - Subscribable.mapOption(form.value, a => a[key]), + View.mapOption(form.value, a => a[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.canCommit, form.isCommitting, @@ -97,9 +97,9 @@ export const focusArrayAt: { return new FormImpl( 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), - Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)), + View.map(form.issues, issues => filterIssuesByPath(issues, path)), form.isValidating, form.canCommit, form.isCommitting, @@ -123,9 +123,9 @@ export const focusTupleAt: { return new FormImpl( path, - Subscribable.mapOption(form.value, values => values[index]), + View.mapOption(form.value, values => values[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.canCommit, form.isCommitting, @@ -149,9 +149,9 @@ export const focusChunkAt: { return new FormImpl( 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), - Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)), + View.map(form.issues, issues => filterIssuesByPath(issues, path)), form.isValidating, form.canCommit, form.isCommitting, diff --git a/packages/effect-fc-next/src/Mutation.ts b/packages/effect-fc-next/src/Mutation.ts index 6ad8140..3e13174 100644 --- a/packages/effect-fc-next/src/Mutation.ts +++ b/packages/effect-fc-next/src/Mutation.ts @@ -1,26 +1,26 @@ import { type Context, Effect, Equal, Exit, type Fiber, Option, Pipeable, Predicate, type Scope, Stream, SubscriptionRef } from "effect" import { AsyncResult } from "effect/unstable/reactivity" import * as Lens from "./Lens.js" -import type * as Subscribable from "./Subscribable.js" +import * as View from "./View.js" export const MutationTypeId: unique symbol = Symbol.for("@effect-fc/Mutation/Mutation") export type MutationTypeId = typeof MutationTypeId -export interface Mutation +export interface Mutation extends Pipeable.Pipeable { readonly [MutationTypeId]: MutationTypeId readonly context: Context.Context readonly f: (key: K) => Effect.Effect - readonly latestKey: Subscribable.Subscribable> - readonly fiber: Subscribable.Subscribable>> - readonly result: Subscribable.Subscribable> - readonly latestFinalResult: Subscribable.Subscribable | AsyncResult.Failure>> + readonly latestKey: View.View> + readonly fiber: View.View>> + readonly state: View.View> + readonly latestFinalResult: View.View | AsyncResult.Failure>> mutate(key: K): Effect.Effect | AsyncResult.Failure> - mutateSubscribable(key: K): Effect.Effect>> + mutateView(key: K): Effect.Effect>> } export const isMutation = (u: unknown): u is Mutation => Predicate.hasProperty(u, MutationTypeId) @@ -36,7 +36,7 @@ extends Pipeable.Class implements Mutation { readonly latestKey: Lens.Lens>, readonly fiber: Lens.Lens>>, - readonly result: Lens.Lens>, + readonly state: Lens.Lens>, readonly latestFinalResult: Lens.Lens | AsyncResult.Failure>>, ) { super() @@ -49,7 +49,7 @@ extends Pipeable.Class implements Mutation { Effect.provide(this.context), ) } - mutateSubscribable(key: K): Effect.Effect>> { + mutateView(key: K): Effect.Effect>> { return Lens.set(this.latestKey, Option.some(key)).pipe( Effect.andThen(this.start(key)), Effect.tap(state => Effect.forkScoped(this.watch(state))), @@ -58,7 +58,7 @@ extends Pipeable.Class implements Mutation { } start(key: K): Effect.Effect< - Subscribable.Subscribable>, + View.View>, never, Scope.Scope | R > { @@ -114,13 +114,13 @@ extends Pipeable.Class implements Mutation { } watch( - state: Subscribable.Subscribable> + state: View.View> ): Effect.Effect | AsyncResult.Failure> { - return state.get.pipe( + return View.get(state).pipe( Effect.andThen(initial => Stream.runFoldEffect( - state.changes, + View.changes(state), () => initial, - (_, result) => Effect.as(Lens.set(this.result, result), result), + (_, result) => Effect.as(Lens.set(this.state, result), result), ) as Effect.Effect | AsyncResult.Failure>), Effect.tap(result => Lens.set(this.latestFinalResult, Option.some(result))), ) diff --git a/packages/effect-fc-next/src/Query.ts b/packages/effect-fc-next/src/Query.ts index e9a155b..ff84519 100644 --- a/packages/effect-fc-next/src/Query.ts +++ b/packages/effect-fc-next/src/Query.ts @@ -2,44 +2,44 @@ import { type Cause, type Context, Duration, Effect, Equal, type Equivalence, Ex import { AsyncResult } from "effect/unstable/reactivity" import * as Lens from "./Lens.js" import * as QueryClient from "./QueryClient.js" -import * as Subscribable from "./Subscribable.js" +import * as View from "./View.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: Subscribable.Subscribable + readonly key: View.View readonly keyEquivalence: Equivalence.Equivalence readonly f: (key: K) => Effect.Effect readonly staleTime: Duration.Duration readonly refreshOnWindowFocus: boolean - readonly fiber: Subscribable.Subscribable>> - readonly state: Subscribable.Subscribable> - readonly latestFinalState: Subscribable.Subscribable>> + readonly fiber: View.View>> + readonly state: View.View> + readonly latestFinalState: View.View>> readonly run: Effect.Effect fetch(key: K): Effect.Effect, Cause.NoSuchElementError> - fetchSubscribable(key: K): Effect.Effect>, Cause.NoSuchElementError> + fetchView(key: K): Effect.Effect>, Cause.NoSuchElementError> readonly refresh: Effect.Effect, Cause.NoSuchElementError> - readonly refreshSubscribable: Effect.Effect>, Cause.NoSuchElementError> + readonly refreshView: Effect.Effect>, Cause.NoSuchElementError> readonly invalidateCache: Effect.Effect invalidateCacheEntry(key: K): Effect.Effect } -export interface QueryState { +export interface QueryState { readonly key: K readonly result: AsyncResult.AsyncResult } -export interface FinalQueryState { +export interface FinalQueryState { readonly key: K readonly result: AsyncResult.Success | AsyncResult.Failure } @@ -53,7 +53,7 @@ extends Pipeable.Class implements Query { constructor( readonly context: Context.Context, - readonly key: Subscribable.Subscribable, + readonly key: View.View, readonly keyEquivalence: Equivalence.Equivalence, readonly f: (key: K) => Effect.Effect, @@ -94,7 +94,7 @@ extends Pipeable.Class implements Query { Effect.flatMap(({ BrowserStream }) => this.refreshOnWindowFocus ? Stream.runForEach( BrowserStream.fromEventListenerWindow("focus"), - () => this.refreshSubscribable, + () => this.refreshView, ) : Effect.void ), @@ -127,8 +127,8 @@ extends Pipeable.Class implements Query { ) } - fetchSubscribable(key: K): Effect.Effect< - Subscribable.Subscribable>, + fetchView(key: K): Effect.Effect< + View.View>, Cause.NoSuchElementError > { return Effect.gen({ self: this }, function*() { @@ -166,8 +166,8 @@ extends Pipeable.Class implements Query { ) } - get refreshSubscribable(): Effect.Effect< - Subscribable.Subscribable>, + get refreshView(): Effect.Effect< + View.View>, Cause.NoSuchElementError > { return Effect.gen({ self: this }, function*() { @@ -194,7 +194,7 @@ extends Pipeable.Class implements Query { startCached( previous: QueryState, ): Effect.Effect< - Subscribable.Subscribable>, + View.View>, Cause.NoSuchElementError, Scope.Scope | QueryClient.QueryClient | R > { @@ -206,7 +206,7 @@ extends Pipeable.Class implements Query { key: previous.key, result: entry.result as AsyncResult.AsyncResult, }) - : Effect.succeed(Subscribable.make({ + : Effect.succeed(View.make({ get: Effect.succeed({ key: previous.key, result: entry.result as AsyncResult.AsyncResult, @@ -226,15 +226,15 @@ extends Pipeable.Class implements Query { start( previous: QueryState, ): Effect.Effect< - Subscribable.Subscribable>, + View.View>, never, Scope.Scope | R > { return Effect.gen({ self: this }, function*() { - const subscribable = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(previous)) + const state = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(previous)) const fiber = yield* Effect.forkScoped(Effect.andThen( - Lens.update(subscribable, previous => AsyncResult.match(previous.result, { + Lens.update(state, previous => AsyncResult.match(previous.result, { onInitial: () => ({ key: previous.key, result: AsyncResult.initial(true), @@ -255,7 +255,7 @@ extends Pipeable.Class implements Query { })), Effect.onExit(this.f(previous.key), exit => Lens.update( - subscribable, + state, previous => Exit.match(exit, { onSuccess: v => ({ key: previous.key, @@ -295,17 +295,17 @@ extends Pipeable.Class implements Query { )) yield* Lens.set(this.fiber, Option.some(fiber)) - return subscribable + return state }) } watch( - subscribable: Subscribable.Subscribable> + view: View.View> ): Effect.Effect, never, QueryClient.QueryClient> { return Effect.gen({ self: this }, function*() { - const initial = yield* subscribable.get + const initial = yield* View.get(view) const final = yield* Stream.runFoldEffect( - subscribable.changes, + View.changes(view), () => initial, (_, state) => Effect.as(Lens.set(this.state, state), state), ) as Effect.Effect> @@ -367,7 +367,7 @@ extends Pipeable.Class implements Query { export declare namespace make { export interface Options { - readonly key: Subscribable.Subscribable, + readonly key: View.View, readonly keyEquivalence?: Equivalence.Equivalence, readonly f: (key: K) => Effect.Effect @@ -396,7 +396,7 @@ export const make = Effect.fnUntraced(function* ( Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none>())), Lens.fromSubscriptionRef(yield* SubscriptionRef.make>({ - key: yield* options.key.get, + key: yield* View.get(options.key), result: AsyncResult.initial(false), })), Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none>())), diff --git a/packages/effect-fc-next/src/QueryClient.ts b/packages/effect-fc-next/src/QueryClient.ts index 2405cbd..946e45f 100644 --- a/packages/effect-fc-next/src/QueryClient.ts +++ b/packages/effect-fc-next/src/QueryClient.ts @@ -1,7 +1,7 @@ import { type Cause, Context, DateTime, Duration, Effect, Equal, Equivalence, Hash, HashMap, type Option, Pipeable, Predicate, Schedule, type Scope, Semaphore, SubscriptionRef } from "effect" import type { AsyncResult } from "effect/unstable/reactivity" import * as Lens from "./Lens.js" -import type * as Subscribable from "./Subscribable.js" +import type * as View from "./View.js" export const QueryClientServiceTypeId: unique symbol = Symbol.for("@effect-fc/QueryClient/QueryClientService") @@ -10,7 +10,7 @@ export type QueryClientServiceTypeId = typeof QueryClientServiceTypeId export interface QueryClientService extends Pipeable.Pipeable { readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId - readonly cache: Subscribable.Subscribable> + readonly cache: View.View> readonly cacheGcTime: Duration.Duration readonly defaultStaleTime: Duration.Duration readonly defaultRefreshOnWindowFocus: boolean diff --git a/packages/effect-fc-next/src/Result.ts b/packages/effect-fc-next/src/Result.ts index 672ae18..1c92d99 100644 --- a/packages/effect-fc-next/src/Result.ts +++ b/packages/effect-fc-next/src/Result.ts @@ -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 { 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") @@ -207,7 +208,7 @@ export const unsafeForkEffect = Effect.fnUntraced(function* effect: Effect.Effect, options?: unsafeForkEffect.Options, NoInfer, P>, ): Effect.fn.Return< - readonly [result: Subscribable.Subscribable, never, never>, fiber: Fiber.Fiber], + readonly [result: View, never, never>, fiber: Fiber.Fiber], never, Scope.Scope | unsafeForkEffect.OutputContext > { @@ -242,7 +243,7 @@ export const forkEffect: { effect: Effect.Effect>>, options?: forkEffect.Options, NoInfer, P>, ): Effect.Effect< - readonly [result: Subscribable.Subscribable, never, never>, fiber: Fiber.Fiber], + readonly [result: View, never, never>, fiber: Fiber.Fiber], never, Scope.Scope | forkEffect.OutputContext > diff --git a/packages/effect-fc-next/src/SubmittableForm.ts b/packages/effect-fc-next/src/SubmittableForm.ts index 79dfe46..a08b59c 100644 --- a/packages/effect-fc-next/src/SubmittableForm.ts +++ b/packages/effect-fc-next/src/SubmittableForm.ts @@ -18,7 +18,7 @@ import * as Form from "./Form.js" import * as Lens from "./Lens.js" import * as Mutation from "./Mutation.js" import * as Result from "./Result.js" -import * as Subscribable from "./Subscribable.js" +import * as View from "./View.js" type FormSchema = Schema.Top & { @@ -39,7 +39,7 @@ extends Form.Form { readonly [value: A, form: SubmittableForm], MA, ME, MR, MP > - readonly validationFiber: Subscribable.Subscribable>, never, never> + readonly validationFiber: View.View>, never, never> readonly run: Effect.Effect readonly submit: Effect.Effect>, Cause.NoSuchElementError> } @@ -50,9 +50,9 @@ extends Pipeable.Class implements SubmittableForm { readonly [SubmittableFormTypeId]: SubmittableFormTypeId = SubmittableFormTypeId readonly path = [] as const readonly encodedValue: Lens.Lens - readonly isValidating: Subscribable.Subscribable - readonly canCommit: Subscribable.Subscribable - readonly isCommitting: Subscribable.Subscribable + readonly isValidating: View.View + readonly canCommit: View.View + readonly isCommitting: View.View constructor( readonly schema: FormSchema, @@ -77,26 +77,26 @@ extends Pipeable.Class implements SubmittableForm { ), lock: Lens.asLensImpl(internalEncodedValue).lock, }) - this.isValidating = Subscribable.map(validationFiber, Option.isSome) - const commitState = Subscribable.zipLatestAll( + this.isValidating = View.map(validationFiber, Option.isSome) + const commitState = View.zipLatestAll( value as any, issues as any, validationFiber as any, mutation.result as any, - ) as unknown as Subscribable.Subscribable, readonly Form.FormIssue[], Option.Option>, Result.Result, ]> - this.canCommit = Subscribable.map( + this.canCommit = View.map( commitState, ([current, currentIssues, fiber, result]: readonly [Option.Option, readonly Form.FormIssue[], Option.Option>, Result.Result]) => Option.isSome(current) && currentIssues.length === 0 && Option.isNone(fiber) && !(Result.isRunning(result) || Result.hasRefreshingFlag(result)), ) - this.isCommitting = Subscribable.map( + this.isCommitting = View.map( mutation.result, result => Result.isRunning(result) || Result.hasRefreshingFlag(result), ) diff --git a/packages/effect-fc-next/src/Subscribable.ts b/packages/effect-fc-next/src/Subscribable.ts deleted file mode 100644 index e8fafe6..0000000 --- a/packages/effect-fc-next/src/Subscribable.ts +++ /dev/null @@ -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 = []>( - ...elements: T -): Subscribable.Subscribable< - [T[number]] extends [never] - ? never - : { [K in keyof T]: T[K] extends Subscribable.Subscribable ? A : never }, - [T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable ? E : never, - [T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable ? 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[number]] extends [never] - ? never - : { [K in keyof T]: T[K] extends Subscribable.Subscribable ? A : never } - - export interface Options { - readonly equivalence?: Equivalence.Equivalence - } -} - -export const useAll = Effect.fnUntraced(function* []>( - elements: T, - options?: useAll.Options>>, -): Effect.fn.Return< - useAll.Success, - [T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable ? E : never, - [T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable ? 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 | undefined) ?? Equivalence.Array(Equivalence.strictEqual())), - Stream.runForEach(v => - Effect.sync(() => setReactStateValue(v)) - ), - Effect.forkScoped, - ), elements) - - return reactStateValue as any -}) diff --git a/packages/effect-fc-next/src/SynchronizedForm.ts b/packages/effect-fc-next/src/SynchronizedForm.ts index 330628a..96d6915 100644 --- a/packages/effect-fc-next/src/SynchronizedForm.ts +++ b/packages/effect-fc-next/src/SynchronizedForm.ts @@ -17,7 +17,7 @@ import { } from "effect" import * as Form from "./Form.js" import * as Lens from "./Lens.js" -import * as Subscribable from "./Subscribable.js" +import * as View from "./View.js" type FormSchema = Schema.Top & { @@ -43,7 +43,7 @@ export interface SynchronizedForm< readonly schema: FormSchema readonly context: Context.Context readonly target: Lens.Lens - readonly validationFiber: Subscribable.Subscribable>, never, never> + readonly validationFiber: View.View>, never, never> readonly run: Effect.Effect } @@ -59,10 +59,10 @@ export class SynchronizedFormImpl< readonly [Form.FormTypeId]: Form.FormTypeId = Form.FormTypeId readonly [SynchronizedFormTypeId]: SynchronizedFormTypeId = SynchronizedFormTypeId readonly path = [] as const - readonly value: Subscribable.Subscribable, TER, never> + readonly value: View.View, TER, never> readonly encodedValue: Lens.Lens - readonly isValidating: Subscribable.Subscribable - readonly canCommit: Subscribable.Subscribable + readonly isValidating: View.View + readonly canCommit: View.View constructor( readonly schema: FormSchema, @@ -75,7 +75,7 @@ export class SynchronizedFormImpl< readonly runSemaphore: Semaphore.Semaphore, ) { super() - this.value = Subscribable.make({ + this.value = View.make({ get: Effect.provide(Effect.map(target.get, Option.some), context), changes: Stream.provideContext( target.changes.pipe( @@ -94,13 +94,13 @@ export class SynchronizedFormImpl< ), lock: Lens.asLensImpl(internalEncodedValue).lock, }) as unknown as Lens.Lens - this.isValidating = Subscribable.map(validationFiber, Option.isSome) - const commitState = Subscribable.zipLatestAll(issues as any, validationFiber as any, isCommitting as any) as unknown as Subscribable.Subscribable>, boolean, ]> - this.canCommit = Subscribable.map( + this.canCommit = View.map( commitState, ([currentIssues, fiber, committing]) => currentIssues.length === 0 && Option.isNone(fiber) && !committing, ) diff --git a/packages/effect-fc-next/src/View.ts b/packages/effect-fc-next/src/View.ts new file mode 100644 index 0000000..04ab6f3 --- /dev/null +++ b/packages/effect-fc-next/src/View.ts @@ -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[number]] extends [never] + ? never + : { [K in keyof T]: T[K] extends View.View ? A : never } + + export interface Options { + readonly equivalence?: Equivalence.Equivalence + } +} + +export const useAll = Effect.fnUntraced(function* []>( + elements: T, + options?: useAll.Options>>, +): Effect.fn.Return< + useAll.Success, + [T[number]] extends [never] ? never : T[number] extends View.View ? E : never, + [T[number]] extends [never] ? never : T[number] extends View.View ? 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 | undefined) ?? Equivalence.Array(Equivalence.strictEqual())), + Stream.runForEach(v => + Effect.sync(() => setReactStateValue(v)) + ), + Effect.forkScoped, + ), elements) + + return reactStateValue as any +}) diff --git a/packages/effect-fc-next/src/index.ts b/packages/effect-fc-next/src/index.ts index 8829e81..ca7e03d 100644 --- a/packages/effect-fc-next/src/index.ts +++ b/packages/effect-fc-next/src/index.ts @@ -13,5 +13,5 @@ export * as Result from "./Result.js" export * as SetStateAction from "./SetStateAction.js" export * as Stream from "./Stream.js" export * as SubmittableForm from "./SubmittableForm.js" -export * as Subscribable from "./Subscribable.js" export * as SynchronizedForm from "./SynchronizedForm.js" +export * as View from "./View.js" diff --git a/packages/effect-fc-next/test/Subscribable.test.tsx b/packages/effect-fc-next/test/View.test.tsx similarity index 60% rename from packages/effect-fc-next/test/Subscribable.test.tsx rename to packages/effect-fc-next/test/View.test.tsx index 5641d1b..3065a8e 100644 --- a/packages/effect-fc-next/test/Subscribable.test.tsx +++ b/packages/effect-fc-next/test/View.test.tsx @@ -1,10 +1,10 @@ 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 { describe, expect, it } from "vitest" import * as Component from "../src/Component.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 () => { @@ -18,42 +18,7 @@ const makeRuntime = async () => { } } -describe("Subscribable", () => { - 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 = [] - - 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)) - }) - +describe("View", () => { it("useAll returns the latest values and rerenders when any input changes", async () => { const { runtime, effectRuntime, dispose } = await makeRuntime() const countRef = await Effect.runPromise(SubscriptionRef.make(1)) @@ -61,8 +26,8 @@ describe("Subscribable", () => { const count = Lens.fromSubscriptionRef(countRef) const label = Lens.fromSubscriptionRef(labelRef) - const Probe = Component.makeUntraced("SubscribableUseAllProbe")(function*() { - const [currentCount, currentLabel] = yield* Subscribable.useAll([count, label]) + const Probe = Component.makeUntraced("ViewUseAllProbe")(function*() { + const [currentCount, currentLabel] = yield* View.useAll([count, label]) return
{`${currentCount}:${currentLabel}`}
}).pipe( @@ -94,8 +59,8 @@ describe("Subscribable", () => { const item = Lens.fromSubscriptionRef(itemRef) const flag = Lens.fromSubscriptionRef(flagRef) - const Probe = Component.makeUntraced("SubscribableUseAllEquivalenceProbe")(function*() { - const [currentItem, currentFlag] = yield* Subscribable.useAll([item, flag], { + const Probe = Component.makeUntraced("ViewUseAllEquivalenceProbe")(function*() { + const [currentItem, currentFlag] = yield* View.useAll([item, flag], { equivalence: ([selfItem, selfFlag], [thatItem, thatFlag]) => selfItem.id === thatItem.id && selfFlag === thatFlag, }) diff --git a/packages/example-next/src/main.tsx b/packages/example-next/src/main.tsx index 125ca5b..3b4d312 100644 --- a/packages/example-next/src/main.tsx +++ b/packages/example-next/src/main.tsx @@ -1,5 +1,5 @@ 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 { createRoot } from "react-dom/client" import "./index.css" @@ -10,7 +10,7 @@ const Counter = Component.make("Counter")(function*() { SubscriptionRef.make(0), Lens.fromSubscriptionRef, )) - const [value] = yield* Subscribable.useAll([count]) + const [value] = yield* View.useAll([count]) const increment = yield* Component.useCallbackSync( () => Lens.update(count, n => n + 1), [count],