43 Commits

Author SHA1 Message Date
Julien Valverdé
6f296d601b Version bump
Some checks failed
Lint / lint (push) Successful in 16s
Test build / test-build (pull_request) Failing after 50s
2026-03-15 23:38:39 +01:00
Julien Valverdé
57c0703014 Docs work
All checks were successful
Lint / lint (push) Successful in 16s
2026-03-15 23:08:38 +01:00
Julien Valverdé
c36c2726c6 Docs work
All checks were successful
Lint / lint (push) Successful in 16s
2026-03-15 21:41:32 +01:00
Julien Valverdé
631819f16f Fix form example
All checks were successful
Lint / lint (push) Successful in 15s
2026-03-13 21:05:47 +01:00
Julien Valverdé
32e43707b6 Merge branch 'next' into docs
All checks were successful
Lint / lint (push) Successful in 14s
2026-03-11 21:53:17 +01:00
Julien Valverdé
2457b1c536 Fix
All checks were successful
Lint / lint (push) Successful in 11s
2026-03-11 21:42:19 +01:00
Julien Valverdé
3cb3f6d103 Update docs
All checks were successful
Lint / lint (push) Successful in 11s
2026-03-11 21:20:20 +01:00
Julien Valverdé
46d7aacc69 Add comments
All checks were successful
Lint / lint (push) Successful in 16s
2026-03-10 20:51:04 +01:00
Julien Valverdé
2f118c5f98 Fix
All checks were successful
Lint / lint (push) Successful in 11s
2026-03-10 20:49:13 +01:00
Julien Valverdé
0ba00a0b4f Make nonReactiveTags a Component option
All checks were successful
Lint / lint (push) Successful in 12s
2026-03-10 20:48:31 +01:00
Julien Valverdé
c644f8c44b Fix Async docs
All checks were successful
Lint / lint (push) Successful in 12s
2026-03-10 19:46:44 +01:00
Julien Valverdé
6917c72101 Add comments
All checks were successful
Lint / lint (push) Successful in 12s
2026-03-10 03:18:45 +01:00
Julien Valverdé
9b3ce62d3e Fix
All checks were successful
Lint / lint (push) Successful in 12s
2026-03-10 02:43:33 +01:00
Julien Valverdé
8b69d4e500 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2026-03-10 02:35:43 +01:00
Julien Valverdé
a9c0590b7c Fix Async example
All checks were successful
Lint / lint (push) Successful in 41s
2026-03-10 02:22:17 +01:00
Julien Valverdé
b63d1ab2c7 Fix
All checks were successful
Lint / lint (push) Successful in 32s
2026-03-09 20:21:18 +01:00
Julien Valverdé
df86af839e Fix Async
All checks were successful
Lint / lint (push) Successful in 12s
2026-03-07 19:45:23 +01:00
Julien Valverdé
dbe42aadb1 Fix
All checks were successful
Lint / lint (push) Successful in 42s
2026-03-05 11:39:51 +01:00
Julien Valverdé
355e179fbd Fix
All checks were successful
Lint / lint (push) Successful in 11s
2026-03-03 16:48:27 +01:00
Julien Valverdé
8dd40d3365 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2026-03-03 15:39:38 +01:00
Julien Valverdé
929f835e94 Restore Async
All checks were successful
Lint / lint (push) Successful in 11s
2026-03-03 15:35:23 +01:00
Julien Valverdé
1f47887643 Fix
All checks were successful
Lint / lint (push) Successful in 11s
2026-03-03 13:47:48 +01:00
Julien Valverdé
3794f56a86 Async example
All checks were successful
Lint / lint (push) Successful in 11s
2026-03-03 13:37:27 +01:00
Julien Valverdé
7f8f91bfc5 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2026-03-03 13:20:19 +01:00
Julien Valverdé
45b38d6c1f Refactor Async
All checks were successful
Lint / lint (push) Successful in 12s
2026-03-03 13:17:47 +01:00
Julien Valverdé
2080d35b2c Fix
All checks were successful
Lint / lint (push) Successful in 40s
2026-03-02 04:02:21 +01:00
Julien Valverdé
346ba9066b Fix
All checks were successful
Lint / lint (push) Successful in 44s
2026-03-01 14:34:50 +01:00
Julien Valverdé
6a6733dc8a Refactor Query
Some checks failed
Lint / lint (push) Failing after 12s
2026-02-28 03:07:10 +01:00
Julien Valverdé
f8a1220f29 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2026-02-27 14:53:50 +01:00
Julien Valverdé
b3fe4a0946 Fix
All checks were successful
Lint / lint (push) Successful in 17s
2026-02-27 14:44:54 +01:00
Julien Valverdé
2766e86f5d Fix
All checks were successful
Lint / lint (push) Successful in 12s
2026-02-27 14:14:54 +01:00
Julien Valverdé
21028fd75b Refactor
All checks were successful
Lint / lint (push) Successful in 12s
2026-02-27 13:21:56 +01:00
Julien Valverdé
8e81ec85de Fix
All checks were successful
Lint / lint (push) Successful in 12s
2026-02-27 10:49:54 +01:00
Julien Valverdé
97246845da useContext -> useContextFromLayer
All checks were successful
Lint / lint (push) Successful in 12s
2026-02-27 10:45:08 +01:00
Julien Valverdé
2c78b17f52 Update comments
Some checks failed
Lint / lint (push) Failing after 1m4s
2026-02-27 10:16:52 +01:00
Julien Valverdé
6c14495693 Improve comments
All checks were successful
Lint / lint (push) Successful in 13s
2026-02-25 22:33:18 +01:00
Julien Valverdé
93156736d7 Merge branch 'next' into docs
All checks were successful
Lint / lint (push) Successful in 15s
2026-02-25 03:19:55 +01:00
Julien Valverdé
a73da25b8c Fix
All checks were successful
Lint / lint (push) Successful in 12s
2026-02-25 03:18:24 +01:00
Julien Valverdé
d0bc4e4903 Refactor Component
All checks were successful
Lint / lint (push) Successful in 43s
2026-02-25 03:09:51 +01:00
Julien Valverdé
19527856fb Add doc intro
All checks were successful
Lint / lint (push) Successful in 15s
2026-02-24 01:54:17 +01:00
Julien Valverdé
2dc7381b3c Add docs package
All checks were successful
Lint / lint (push) Successful in 44s
2026-02-24 01:07:59 +01:00
0ae55bd02c Update dependency @effect/language-service to ^0.75.0 (#34)
All checks were successful
Lint / lint (push) Successful in 14s
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@effect/language-service](https://github.com/Effect-TS/language-service) | [`^0.72.0` → `^0.75.0`](https://renovatebot.com/diffs/npm/@effect%2flanguage-service/0.72.1/0.75.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@effect%2flanguage-service/0.75.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@effect%2flanguage-service/0.72.1/0.75.1?slim=true) |

---

### Release Notes

<details>
<summary>Effect-TS/language-service (@&#8203;effect/language-service)</summary>

### [`v0.75.1`](https://github.com/Effect-TS/language-service/releases/tag/%40effect/language-service%400.75.1)

[Compare Source](https://github.com/Effect-TS/language-service/compare/@effect/language-service@0.75.0...@effect/language-service@0.75.1)

##### Patch Changes

- [#&#8203;647](https://github.com/Effect-TS/language-service/pull/647) [`489e3f0`](489e3f0572) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Expose diagnostic quick fixes as refactoring actions to work around TypeScript's limited quick fix handling in some contexts

- [#&#8203;650](https://github.com/Effect-TS/language-service/pull/650) [`6f568cf`](6f568cf37a) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Fix TypeParser to skip types with generic call signatures. When parsing covariant, contravariant, or invariant types, signatures with type parameters are now correctly rejected instead of being treated as concrete types.

- [#&#8203;649](https://github.com/Effect-TS/language-service/pull/649) [`5858fd1`](5858fd1d87) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Performance improvements: replace `Nano.gen` with `Nano.fn` named functions across diagnostics, refactors, and code generation modules for better performance tracking and reduced runtime overhead. Add conditional `debugPerformance` flag to avoid unnecessary timing collection when not debugging.

### [`v0.75.0`](https://github.com/Effect-TS/language-service/releases/tag/%40effect/language-service%400.75.0)

[Compare Source](https://github.com/Effect-TS/language-service/compare/@effect/language-service@0.74.0...@effect/language-service@0.75.0)

##### Minor Changes

- [#&#8203;645](https://github.com/Effect-TS/language-service/pull/645) [`a8a7d33`](a8a7d33f3a) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Add `ServiceMap.Service` class completion for Effect v4, and fix Schema class completions for v4 (`TaggedErrorClass`, `TaggedClass` now available, `ErrorClass` fully-qualified form fixed, `RequestClass` removed)

### [`v0.74.0`](https://github.com/Effect-TS/language-service/releases/tag/%40effect/language-service%400.74.0)

[Compare Source](https://github.com/Effect-TS/language-service/compare/@effect/language-service@0.73.1...@effect/language-service@0.74.0)

##### Minor Changes

- [#&#8203;641](https://github.com/Effect-TS/language-service/pull/641) [`693e5a5`](693e5a5ef2) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Added Effect v4 support for diagnostics, refactors, and piping features.

  **Diagnostics:**

  - `multipleEffectProvide`: Warns when multiple `Effect.provide` calls are chained, suggesting consolidation
  - `strictEffectProvide`: Warns when using `Effect.provide` with Layer outside of application entry points
  - `missingLayerContext`: Detects missing Layer context requirements
  - `deterministicKeys`: Extended to support `ServiceMap.Service` patterns
  - `leakingRequirements`: Extended to detect leaking requirements in ServiceMap services
  - `schemaSyncInEffect`: Updated with v4-specific method mappings (e.g., `decodeSync` -> `decodeEffect`)

  **Refactors:**

  - `layerMagic`: Automatically compose and build layers based on service dependencies
  - `structuralTypeToSchema`: Convert TypeScript interfaces and type aliases to Effect Schema classes
  - `makeSchemaOpaque`: Enhanced for v4 with support for `Codec`, `DecodingServices`, and `EncodingServices` types
  - `typeToEffectSchema`: Enhanced to support Effect v4 schema patterns

  **Piping:**

  - Added pipe transformation support for Effect v4 including `Effect.fn`, nested pipes, and function call conversions

##### Patch Changes

- [#&#8203;643](https://github.com/Effect-TS/language-service/pull/643) [`68f6d12`](68f6d120ad) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Disable `schemaUnionOfLiterals` diagnostic for Effect v4, as `Schema.Union` of multiple `Schema.Literal` calls is no longer applicable in v4.

### [`v0.73.1`](https://github.com/Effect-TS/language-service/releases/tag/%40effect/language-service%400.73.1)

[Compare Source](https://github.com/Effect-TS/language-service/compare/@effect/language-service@0.73.0...@effect/language-service@0.73.1)

##### Patch Changes

- [#&#8203;639](https://github.com/Effect-TS/language-service/pull/639) [`ff72045`](ff72045531) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Add wildcard (`*`) support for `@effect-diagnostics` comment directives. You can now use `*` as a rule name to apply a severity override to all diagnostics at once, e.g. `@effect-diagnostics *:off` disables all Effect diagnostics from that point on. Rule-specific overrides still take precedence over wildcard overrides.

### [`v0.73.0`](https://github.com/Effect-TS/language-service/releases/tag/%40effect/language-service%400.73.0)

[Compare Source](https://github.com/Effect-TS/language-service/compare/@effect/language-service@0.72.1...@effect/language-service@0.73.0)

##### Minor Changes

- [#&#8203;637](https://github.com/Effect-TS/language-service/pull/637) [`616c2cc`](616c2cc21c) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Add Effect v4 completions support

  - Detect installed Effect version (v3 or v4) and conditionally enable version-specific completions
  - Add `Schema.ErrorClass` and `Schema.RequestClass` completions for Effect v4
  - Disable v3-only completions (`Effect.Service`, `Effect.Tag`, `Schema.TaggedError`, `Schema.TaggedClass`, `Schema.TaggedRequest`, `Context.Tag` self, `Rpc.make` classes, `Schema.brand`, `Model.Class`) when Effect v4 is detected
  - Support lowercase `taggedEnum` in addition to `TaggedEnum` for v4 API compatibility

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4zLjQiLCJ1cGRhdGVkSW5WZXIiOiI0My4yNy4wIiwidGFyZ2V0QnJhbmNoIjoibmV4dCIsImxhYmVscyI6W119-->

Reviewed-on: #34
Co-authored-by: Renovate Bot <renovate-bot@valverde.cloud>
Co-committed-by: Renovate Bot <renovate-bot@valverde.cloud>
2026-02-23 23:32:07 +01:00
Julien Valverdé
bb044e766d Fix README
All checks were successful
Lint / lint (push) Successful in 58s
2026-01-23 01:55:10 +01:00
24 changed files with 825 additions and 1267 deletions

0
.codex
View File

713
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@effect-fc/monorepo", "name": "@effect-fc/monorepo",
"packageManager": "bun@1.3.11", "packageManager": "bun@1.3.6",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"./packages/*" "./packages/*"
@@ -15,12 +15,12 @@
"clean:modules": "turbo clean:modules && rm -rf node_modules" "clean:modules": "turbo clean:modules && rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.9", "@biomejs/biome": "^2.3.11",
"@effect/language-service": "^0.85.0", "@effect/language-service": "^0.75.0",
"@types/bun": "^1.3.11", "@types/bun": "^1.3.6",
"npm-check-updates": "^22.0.0", "npm-check-updates": "^19.3.1",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",
"turbo": "^2.8.21", "turbo": "^2.7.5",
"typescript": "^6.0.2" "typescript": "^5.9.3"
} }
} }

View File

@@ -1,8 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
"root": false,
"extends": "//",
"files": {
"includes": ["./src/**"]
}
}

View File

@@ -92,6 +92,7 @@ pnpm add effect-fc effect react
- Explore the [Tutorial Basics](./tutorial-basics/create-a-document.md) to learn the fundamentals - Explore the [Tutorial Basics](./tutorial-basics/create-a-document.md) to learn the fundamentals
- Check out the [Example Project](https://github.com/your-repo/packages/example) for a complete working application - Check out the [Example Project](https://github.com/your-repo/packages/example) for a complete working application
- Read the [API Documentation](./api/) for detailed reference
## Important Notes ## Important Notes

View File

@@ -15,8 +15,8 @@
"typecheck": "tsc" "typecheck": "tsc"
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "3.10.0", "@docusaurus/core": "3.9.2",
"@docusaurus/preset-classic": "3.10.0", "@docusaurus/preset-classic": "3.9.2",
"@mdx-js/react": "^3.0.0", "@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0", "prism-react-renderer": "^2.3.0",
@@ -24,10 +24,10 @@
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "3.10.0", "@docusaurus/module-type-aliases": "3.9.2",
"@docusaurus/tsconfig": "3.10.0", "@docusaurus/tsconfig": "3.9.2",
"@docusaurus/types": "3.10.0", "@docusaurus/types": "3.9.2",
"typescript": "~6.0.0" "typescript": "~5.6.2"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

View File

@@ -1,10 +1,10 @@
import type {ReactNode} from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import HomepageFeatures from '@site/src/components/HomepageFeatures'; import HomepageFeatures from '@site/src/components/HomepageFeatures';
import Heading from '@theme/Heading'; import Heading from '@theme/Heading';
import Layout from '@theme/Layout';
import clsx from 'clsx';
import type {ReactNode} from 'react';
import styles from './index.module.css'; import styles from './index.module.css';

View File

@@ -1,7 +1,7 @@
{ {
"name": "effect-fc", "name": "effect-fc",
"description": "Write React function components with Effect", "description": "Write React function components with Effect",
"version": "0.2.5", "version": "0.2.4",
"type": "module", "type": "module",
"files": [ "files": [
"./README.md", "./README.md",
@@ -38,14 +38,11 @@
"clean:modules": "rm -rf node_modules" "clean:modules": "rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"@effect/platform-browser": "^0.76.0" "@effect/platform-browser": "^0.74.0"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0", "@types/react": "^19.2.0",
"effect": "^3.21.0", "effect": "^3.19.0",
"react": "^19.2.0" "react": "^19.2.0"
},
"dependencies": {
"effect-lens": "^0.1.4"
} }
} }

View File

@@ -1,99 +1,105 @@
import { Array, Cause, Chunk, type Context, type Duration, Effect, Equal, Exit, Fiber, Function, identity, Option, ParseResult, Pipeable, Predicate, Schema, type Scope, Stream } from "effect" import { Array, Cause, Chunk, type Context, type Duration, Effect, Equal, Exit, Fiber, flow, Hash, HashMap, identity, Option, ParseResult, Pipeable, Predicate, Ref, Schema, type Scope, Stream } from "effect"
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 Mutation from "./Mutation.js" import * as Mutation from "./Mutation.js"
import * as PropertyPath from "./PropertyPath.js"
import * as Result from "./Result.js" import * as Result from "./Result.js"
import * as Subscribable from "./Subscribable.js" import * as Subscribable from "./Subscribable.js"
import * as SubscriptionRef from "./SubscriptionRef.js" import * as SubscriptionRef from "./SubscriptionRef.js"
import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form") export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form")
export type FormTypeId = typeof FormTypeId export type FormTypeId = typeof FormTypeId
export interface Form<out P extends readonly PropertyKey[], in out A, in out I = A, in out ER = never, in out EW = never> export interface Form<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
extends Pipeable.Pipeable { extends Pipeable.Pipeable {
readonly [FormTypeId]: FormTypeId readonly [FormTypeId]: FormTypeId
readonly path: P
readonly value: Subscribable.Subscribable<Option.Option<A>, ER, never>
readonly encodedValue: Lens.Lens<I, ER, EW, never, never>
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[], never, never>
readonly isValidating: Subscribable.Subscribable<boolean, ER, never>
readonly canCommit: Subscribable.Subscribable<boolean, never, never>
readonly isCommitting: Subscribable.Subscribable<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>
extends Pipeable.Class() implements Form<P, A, I, ER, EW> {
readonly [FormTypeId]: FormTypeId = FormTypeId
constructor(
readonly path: P,
readonly value: Subscribable.Subscribable<Option.Option<A>, ER, never>,
readonly encodedValue: Lens.Lens<I, ER, EW, never, never>,
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[], never, never>,
readonly isValidating: Subscribable.Subscribable<boolean, never, never>,
readonly canCommit: Subscribable.Subscribable<boolean, never, never>,
readonly isCommitting: Subscribable.Subscribable<boolean, never, never>,
) {
super()
}
}
export const SubmittableFormTypeId: unique symbol = Symbol.for("@effect-fc/Form/SubmittableForm")
export type SubmittableFormTypeId = typeof SubmittableFormTypeId
export interface SubmittableForm<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
extends Form<readonly [], A, I, never, never> {
readonly [SubmittableFormTypeId]: SubmittableFormTypeId
readonly schema: Schema.Schema<A, I, R> readonly schema: Schema.Schema<A, I, R>
readonly context: Context.Context<Scope.Scope | R> readonly context: Context.Context<Scope.Scope | R>
readonly mutation: Mutation.Mutation< readonly mutation: Mutation.Mutation<
readonly [value: A, form: SubmittableForm<A, I, R, unknown, unknown, unknown>], readonly [value: A, form: Form<A, I, R, unknown, unknown, unknown>],
MA, ME, MR, MP MA, ME, MR, MP
> >
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>, never, never> readonly autosubmit: boolean
readonly debounce: Option.Option<Duration.DurationInput>
readonly value: Subscribable.Subscribable<Option.Option<A>>
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
readonly error: Subscribable.Subscribable<Option.Option<ParseResult.ParseError>>
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>
readonly canSubmit: Subscribable.Subscribable<boolean>
field<const P extends PropertyPath.Paths<I>>(
path: P
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>>
readonly run: Effect.Effect<void> readonly run: Effect.Effect<void>
readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException>
} }
export class SubmittableFormImpl<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never> export class FormImpl<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
extends Pipeable.Class() implements SubmittableForm<A, I, R, MA, ME, MR, MP> { extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
readonly [FormTypeId]: FormTypeId = FormTypeId readonly [FormTypeId]: FormTypeId = FormTypeId
readonly [SubmittableFormTypeId]: SubmittableFormTypeId = SubmittableFormTypeId
readonly path = [] as const
constructor( constructor(
readonly schema: Schema.Schema<A, I, R>, readonly schema: Schema.Schema<A, I, R>,
readonly context: Context.Context<Scope.Scope | R>, readonly context: Context.Context<Scope.Scope | R>,
readonly mutation: Mutation.Mutation< readonly mutation: Mutation.Mutation<
readonly [value: A, form: SubmittableForm<A, I, R, unknown, unknown, unknown>], readonly [value: A, form: Form<A, I, R, unknown, unknown, unknown>],
MA, ME, MR, MP MA, ME, MR, MP
>, >,
readonly value: Lens.Lens<Option.Option<A>, never, never, never, never>, readonly autosubmit: boolean,
readonly encodedValue: Lens.Lens<I, never, never, never, never>, readonly debounce: Option.Option<Duration.DurationInput>,
readonly issues: Lens.Lens<readonly ParseResult.ArrayFormatterIssue[], never, never, never, never>,
readonly validationFiber: Lens.Lens<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>, never, never, never, never>,
readonly isValidating: Subscribable.Subscribable<boolean, never, never>,
readonly canCommit: Subscribable.Subscribable<boolean, never, never>, readonly value: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
readonly isCommitting: Subscribable.Subscribable<boolean, never, never>, readonly encodedValue: SubscriptionRef.SubscriptionRef<I>,
readonly error: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
readonly validationFiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>,
readonly runSemaphore: Effect.Semaphore, readonly runSemaphore: Effect.Semaphore,
readonly fieldCache: Ref.Ref<HashMap.HashMap<FormFieldKey, FormField<unknown, unknown>>>,
) { ) {
super() super()
this.canSubmit = Subscribable.map(
Subscribable.zipLatestAll(this.value, this.error, this.validationFiber, this.mutation.result),
([value, error, validationFiber, result]) => (
Option.isSome(value) &&
Option.isNone(error) &&
Option.isNone(validationFiber) &&
!(Result.isRunning(result) || Result.hasRefreshingFlag(result))
),
)
} }
field<const P extends PropertyPath.Paths<I>>(
path: P
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>> {
const key = new FormFieldKey(path)
return this.fieldCache.pipe(
Effect.map(HashMap.get(key)),
Effect.flatMap(Option.match({
onSome: v => Effect.succeed(v as FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>),
onNone: () => Effect.tap(
Effect.succeed(makeFormField(this as Form<A, I, R, MA, ME, MR, MP>, path)),
v => Ref.update(this.fieldCache, HashMap.set(key, v as FormField<unknown, unknown>)),
),
})),
)
}
readonly canSubmit: Subscribable.Subscribable<boolean>
get run(): Effect.Effect<void> { get run(): Effect.Effect<void> {
return this.runSemaphore.withPermits(1)(Stream.runForEach( return this.runSemaphore.withPermits(1)(Stream.runForEach(
this.encodedValue.changes, this.encodedValue.changes.pipe(
Option.isSome(this.debounce) ? Stream.debounce(this.debounce.value) : identity
),
encodedValue => Lens.get(this.validationFiber).pipe( encodedValue => this.validationFiber.pipe(
Effect.andThen(Option.match({ Effect.andThen(Option.match({
onSome: Fiber.interrupt, onSome: Fiber.interrupt,
onNone: () => Effect.void, onNone: () => Effect.void,
@@ -104,22 +110,23 @@ extends Pipeable.Class() implements SubmittableForm<A, I, R, MA, ME, MR, MP> {
exit => Effect.andThen( exit => Effect.andThen(
Exit.matchEffect(exit, { Exit.matchEffect(exit, {
onSuccess: v => Effect.andThen( onSuccess: v => Effect.andThen(
Lens.set(this.value, Option.some(v)), Ref.set(this.value, Option.some(v)),
Lens.set(this.issues, Array.empty()), Ref.set(this.error, Option.none()),
), ),
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), { onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
onSome: e => Effect.flatMap( onSome: e => Ref.set(this.error, Option.some(e)),
ParseResult.ArrayFormatter.formatError(e),
v => Lens.set(this.issues, v),
),
onNone: () => Effect.void, onNone: () => Effect.void,
}), }),
}), }),
Lens.set(this.validationFiber, Option.none()), Ref.set(this.validationFiber, Option.none()),
), ),
)).pipe( )).pipe(
Effect.tap(fiber => Lens.set(this.validationFiber, Option.some(fiber))), Effect.tap(fiber => Ref.set(this.validationFiber, Option.some(fiber))),
Effect.andThen(Fiber.join), Effect.andThen(Fiber.join),
Effect.andThen(value => this.autosubmit
? Effect.asVoid(Effect.forkScoped(this.submitValue(value)))
: Effect.void
),
Effect.forkScoped, Effect.forkScoped,
) )
), ),
@@ -129,7 +136,7 @@ extends Pipeable.Class() implements SubmittableForm<A, I, R, MA, ME, MR, MP> {
} }
get submit(): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> { get submit(): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> {
return Lens.get(this.value).pipe( return this.value.pipe(
Effect.andThen(identity), Effect.andThen(identity),
Effect.andThen(value => this.submitValue(value)), Effect.andThen(value => this.submitValue(value)),
) )
@@ -146,385 +153,141 @@ extends Pipeable.Class() implements SubmittableForm<A, I, R, MA, ME, MR, MP> {
e => e._tag === "ParseError", e => e._tag === "ParseError",
), ),
{ {
onSome: e => Effect.flatMap( onSome: e => Ref.set(this.error, Option.some(e)),
ParseResult.ArrayFormatter.formatError(e),
v => Lens.set(this.issues, v),
),
onNone: () => Effect.void, onNone: () => Effect.void,
}, },
) )
: Effect.void : Effect.void
), ),
this.canCommit.get, this.canSubmit.get,
) )
} }
} }
export const isForm = (u: unknown): u is Form<unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, FormTypeId)
export const SynchronizedFormTypeId: unique symbol = Symbol.for("@effect-fc/Form/SynchronizedForm") export declare namespace make {
export type SynchronizedFormTypeId = typeof SynchronizedFormTypeId
export interface SynchronizedForm<
in out A,
in out I = A,
in out R = never,
in out TER = never,
in out TEW = never,
in out TRR = never,
in out TRW = never,
> extends Form<readonly [], A, I, never, never> {
readonly [SynchronizedFormTypeId]: SynchronizedFormTypeId
readonly schema: Schema.Schema<A, I, R>
readonly context: Context.Context<Scope.Scope | R | TRR | TRW>
readonly target: Lens.Lens<A, TER, TEW, TRR, TRW>
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>, never, never>
readonly run: Effect.Effect<void, ParseResult.ParseError | TER | TEW>
}
export class SynchronizedFormImpl<
in out A,
in out I = A,
in out R = never,
in out TER = never,
in out TEW = never,
in out TRR = never,
in out TRW = never,
> extends Pipeable.Class() implements SynchronizedForm<A, I, R, TER, TEW, TRR, TRW> {
readonly [FormTypeId]: FormTypeId = FormTypeId
readonly [SynchronizedFormTypeId]: SynchronizedFormTypeId = SynchronizedFormTypeId
readonly path = [] as const
constructor(
readonly schema: Schema.Schema<A, I, R>,
readonly context: Context.Context<Scope.Scope | R | TRR | TRW>,
readonly target: Lens.Lens<A, TER, TEW, TRR, TRW>,
readonly value: Lens.Lens<Option.Option<A>, never, never, never, never>,
readonly encodedValue: Lens.Lens<I, never, never, never, never>,
readonly issues: Lens.Lens<readonly ParseResult.ArrayFormatterIssue[], never, never, never, never>,
readonly validationFiber: Lens.Lens<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>, never, never, never, never>,
readonly isValidating: Subscribable.Subscribable<boolean, never, never>,
readonly canCommit: Subscribable.Subscribable<boolean, never, never>,
readonly isCommitting: Subscribable.Subscribable<boolean, never, never>,
readonly runSemaphore: Effect.Semaphore,
) {
super()
}
get run(): Effect.Effect<void, ParseResult.ParseError | TER | TEW> {
return this.runSemaphore.withPermits(1)(Effect.provide(
Effect.all([
Stream.runForEach(
this.encodedValue.changes,
encodedValue => Lens.get(this.validationFiber).pipe(
Effect.andThen(Option.match({
onSome: Fiber.interrupt,
onNone: () => Effect.void,
})),
Effect.andThen(
Effect.forkScoped(Effect.onExit(
Schema.decode(this.schema, { errors: "all" })(encodedValue),
exit => Effect.andThen(
Exit.matchEffect(exit, {
onSuccess: v => Lens.set(this.value, Option.some(v)).pipe(
Effect.andThen(Lens.set(this.issues, Array.empty())),
),
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
onSome: e => Effect.flatMap(
ParseResult.ArrayFormatter.formatError(e),
v => Lens.set(this.issues, v),
),
onNone: () => Effect.void,
}),
}),
Lens.set(this.validationFiber, Option.none()),
),
)).pipe(
Effect.tap(fiber => Lens.set(this.validationFiber, Option.some(fiber))),
Effect.andThen(Fiber.join),
Effect.tap(value => Lens.set(this.target, value)),
Effect.forkScoped,
)
),
),
),
Stream.runForEach(
Stream.drop(this.target.changes, 1),
targetValue => Schema.encode(this.schema, { errors: "all" })(targetValue).pipe(
Effect.flatMap(encodedValue => Effect.whenEffect(
Lens.set(this.encodedValue, encodedValue),
Effect.map(
Lens.get(this.encodedValue),
currentEncodedValue => !Equal.equals(encodedValue, currentEncodedValue),
),
)),
),
),
], { concurrency: "unbounded", discard: true }),
this.context,
))
}
}
export const isForm = (u: unknown): u is Form<readonly PropertyKey[], unknown, unknown> => Predicate.hasProperty(u, FormTypeId)
export const isSubmittableForm = (u: unknown): u is SubmittableForm<unknown, unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, SubmittableFormTypeId)
export const isSynchronizedForm = (u: unknown): u is SynchronizedForm<unknown, unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, SynchronizedFormTypeId)
const falseSubscribable: Subscribable.Subscribable<boolean, never, never> = Subscribable.make({
get: Effect.succeed(false),
changes: Stream.make(false),
})
export declare namespace makeSubmittable {
export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never> export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
extends Mutation.make.Options< extends Mutation.make.Options<
readonly [value: NoInfer<A>, form: SubmittableForm<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>], readonly [value: NoInfer<A>, form: Form<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>],
MA, ME, MR, MP MA, ME, MR, MP
> { > {
readonly schema: Schema.Schema<A, I, R> readonly schema: Schema.Schema<A, I, R>
readonly initialEncodedValue: NoInfer<I> readonly initialEncodedValue: NoInfer<I>
readonly autosubmit?: boolean
readonly debounce?: Duration.DurationInput
} }
} }
export const makeSubmittable = Effect.fnUntraced(function* <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>( export const make = Effect.fnUntraced(function* <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
options: makeSubmittable.Options<A, I, R, MA, ME, MR, MP> options: make.Options<A, I, R, MA, ME, MR, MP>
): Effect.fn.Return< ): Effect.fn.Return<
SubmittableForm<A, I, R, MA, ME, Result.forkEffect.OutputContext<MR, MP>, MP>, Form<A, I, R, MA, ME, Result.forkEffect.OutputContext<MA, ME, MR, MP>, MP>,
never, never,
Scope.Scope | R | Result.forkEffect.OutputContext<MR, MP> Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP>
> { > {
const mutation = yield* Mutation.make(options) return new FormImpl(
const valueLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<A>()))
const issuesLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make<readonly ParseResult.ArrayFormatterIssue[]>(Array.empty()))
const validationFiberLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>()))
return new SubmittableFormImpl(
options.schema, options.schema,
yield* Effect.context<Scope.Scope | R>(), yield* Effect.context<Scope.Scope | R>(),
mutation, yield* Mutation.make(options),
options.autosubmit ?? false,
Option.fromNullable(options.debounce),
valueLens, yield* SubscriptionRef.make(Option.none<A>()),
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(options.initialEncodedValue)), yield* SubscriptionRef.make(options.initialEncodedValue),
issuesLens, yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>()),
validationFiberLens, yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>()),
Subscribable.map(validationFiberLens, Option.isSome),
Subscribable.map(
Subscribable.zipLatestAll(valueLens, issuesLens, validationFiberLens, mutation.result),
([value, issues, validationFiber, result]) => (
Option.isSome(value) &&
Array.isEmptyReadonlyArray(issues) &&
Option.isNone(validationFiber) &&
!(Result.isRunning(result) || Result.hasRefreshingFlag(result))
),
),
Subscribable.map(mutation.result, result => Result.isRunning(result) || Result.hasRefreshingFlag(result)),
yield* Effect.makeSemaphore(1), yield* Effect.makeSemaphore(1),
yield* Ref.make(HashMap.empty<FormFieldKey, FormField<unknown, unknown>>()),
) )
}) })
export declare namespace serviceSubmittable { export declare namespace service {
export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never> export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
extends makeSubmittable.Options<A, I, R, MA, ME, MR, MP> {} extends make.Options<A, I, R, MA, ME, MR, MP> {}
} }
export const serviceSubmittable = <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>( export const service = <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
options: serviceSubmittable.Options<A, I, R, MA, ME, MR, MP> options: service.Options<A, I, R, MA, ME, MR, MP>
): Effect.Effect< ): Effect.Effect<
SubmittableForm<A, I, R, MA, ME, Result.forkEffect.OutputContext<MR, MP>, MP>, Form<A, I, R, MA, ME, Result.forkEffect.OutputContext<MA, ME, MR, MP>, MP>,
never, never,
Scope.Scope | R | Result.forkEffect.OutputContext<MR, MP> Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP>
> => Effect.tap( > => Effect.tap(
makeSubmittable(options), make(options),
form => Effect.forkScoped(form.run), form => Effect.forkScoped(form.run),
) )
export declare namespace makeSynchronized {
export interface Options<in out A, in out I = A, in out R = never, in out TER = never, in out TEW = never, in out TRR = never, in out TRW = never> { export const FormFieldTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormField")
readonly schema: Schema.Schema<A, I, R> export type FormFieldTypeId = typeof FormFieldTypeId
readonly target: Lens.Lens<A, TER, TEW, TRR, TRW>
readonly initialEncodedValue: NoInfer<I> export interface FormField<in out A, in out I = A>
extends Pipeable.Pipeable {
readonly [FormFieldTypeId]: FormFieldTypeId
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
readonly isValidating: Subscribable.Subscribable<boolean>
readonly isSubmitting: Subscribable.Subscribable<boolean>
}
class FormFieldImpl<in out A, in out I = A>
extends Pipeable.Class() implements FormField<A, I> {
readonly [FormFieldTypeId]: FormFieldTypeId = FormFieldTypeId
constructor(
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>,
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>,
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>,
readonly isValidating: Subscribable.Subscribable<boolean>,
readonly isSubmitting: Subscribable.Subscribable<boolean>,
) {
super()
} }
} }
export const makeSynchronized = Effect.fnUntraced(function* <A, I = A, R = never, TER = never, TEW = never, TRR = never, TRW = never>( const FormFieldKeyTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormFieldKey")
options: makeSynchronized.Options<A, I, R, TER, TEW, TRR, TRW> type FormFieldKeyTypeId = typeof FormFieldKeyTypeId
): Effect.fn.Return<
SynchronizedForm<A, I, R, TER, TEW, TRR, TRW>,
ParseResult.ParseError | TER,
Scope.Scope | R | TRR | TRW
> {
const valueLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<A>()))
const issuesLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make<readonly ParseResult.ArrayFormatterIssue[]>(Array.empty()))
const validationFiberLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>()))
const initialEncodedValue = yield* Lens.get(options.target).pipe(
Effect.flatMap(Schema.encode(options.schema, { errors: "all" })),
)
return new SynchronizedFormImpl( class FormFieldKey implements Equal.Equal {
options.schema, readonly [FormFieldKeyTypeId]: FormFieldKeyTypeId = FormFieldKeyTypeId
yield* Effect.context<Scope.Scope | R | TRR | TRW>(), constructor(readonly path: PropertyPath.PropertyPath) {}
options.target,
valueLens, [Equal.symbol](that: Equal.Equal) {
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(initialEncodedValue)), return isFormFieldKey(that) && PropertyPath.equivalence(this.path, that.path)
issuesLens, }
validationFiberLens, [Hash.symbol]() {
Subscribable.map(validationFiberLens, Option.isSome), return Hash.array(this.path)
}
Subscribable.map(
Subscribable.zipLatestAll(valueLens, issuesLens, validationFiberLens),
([value, issues, validationFiber]) => (
Option.isSome(value) &&
Array.isEmptyReadonlyArray(issues) &&
Option.isNone(validationFiber)
),
),
falseSubscribable,
yield* Effect.makeSemaphore(1),
)
})
export declare namespace serviceSynchronized {
export interface Options<in out A, in out I = A, in out R = never, in out TER = never, in out TEW = never, in out TRR = never, in out TRW = never>
extends makeSynchronized.Options<A, I, R, TER, TEW, TRR, TRW> {}
} }
export const serviceSynchronized = <A, I = A, R = never, TER = never, TEW = never, TRR = never, TRW = never>( export const isFormField = (u: unknown): u is FormField<unknown, unknown> => Predicate.hasProperty(u, FormFieldTypeId)
options: serviceSynchronized.Options<A, I, R, TER, TEW, TRR, TRW> const isFormFieldKey = (u: unknown): u is FormFieldKey => Predicate.hasProperty(u, FormFieldKeyTypeId)
): Effect.Effect<
SynchronizedForm<A, I, R, TER, TEW, TRR, TRW>,
ParseResult.ParseError | TER,
Scope.Scope | R | TRR | TRW
> => Effect.tap(
makeSynchronized(options),
form => Effect.forkScoped(form.run),
)
export const makeFormField = <A, I, R, MA, ME, MR, MP, const P extends PropertyPath.Paths<NoInfer<I>>>(
const filterIssuesByPath = ( self: Form<A, I, R, MA, ME, MR, MP>,
issues: readonly ParseResult.ArrayFormatterIssue[], path: P,
path: readonly PropertyKey[], ): FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>> => {
): readonly ParseResult.ArrayFormatterIssue[] => Array.filter(issues, issue => return new FormFieldImpl(
issue.path.length >= path.length && Array.every(path, (p, i) => p === issue.path[i]) Subscribable.mapEffect(self.value, Option.match({
) onSome: v => Option.map(PropertyPath.get(v, path), Option.some),
onNone: () => Option.some(Option.none()),
export const focusObjectField: { })),
<P extends readonly PropertyKey[], A extends object, I extends object, ER, EW, K extends keyof A & keyof I>( SubscriptionSubRef.makeFromPath(self.encodedValue, path),
self: Form<P, A, I, ER, EW>, Subscribable.mapEffect(self.error, Option.match({
key: K, onSome: flow(
): Form<readonly [...P, K], A[K], I[K], ER, EW> ParseResult.ArrayFormatter.formatError,
<P extends readonly PropertyKey[], A extends object, I extends object, ER, EW, K extends keyof A & keyof I>( Effect.map(Array.filter(issue => PropertyPath.equivalence(issue.path, path))),
key: K, ),
): (self: Form<P, A, I, ER, EW>) => Form<readonly [...P, K], A[K], I[K], ER, EW> onNone: () => Effect.succeed([]),
} = Function.dual(2, <P extends readonly PropertyKey[], A extends object, I extends object, ER, EW, K extends keyof A & keyof I>( })),
self: Form<P, A, I, ER, EW>, Subscribable.map(self.validationFiber, Option.isSome),
key: K, Subscribable.map(self.mutation.result, result => Result.isRunning(result) || Result.hasRefreshingFlag(result)),
): Form<readonly [...P, K], A[K], I[K], ER, EW> => {
const form = self as FormImpl<P, A, I, ER, EW>
const path = [...form.path, key] as const
return new FormImpl(
path,
Subscribable.mapOption(form.value, a => a[key]),
Lens.focusObjectField(form.encodedValue, key),
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
form.isValidating,
form.canCommit,
form.isCommitting,
) )
}) }
export const focusArrayAt: {
<P extends readonly PropertyKey[], A extends readonly any[], I extends readonly any[], ER, EW>(
self: Form<P, A, I, ER, EW>,
index: number,
): Form<readonly [...P, number], A[number], I[number], ER | Cause.NoSuchElementException, EW | Cause.NoSuchElementException>
<P extends readonly PropertyKey[], A extends readonly any[], I extends readonly any[], ER, EW>(
index: number,
): (self: Form<P, A, I, ER, EW>) => Form<readonly [...P, number], A[number], I[number], ER | Cause.NoSuchElementException, EW | Cause.NoSuchElementException>
} = Function.dual(2, <P extends readonly PropertyKey[], A extends readonly any[], I extends readonly any[], ER, EW>(
self: Form<P, A, I, ER, EW>,
index: number,
): Form<readonly [...P, number], A[number], I[number], ER | Cause.NoSuchElementException, EW | Cause.NoSuchElementException> => {
const form = self as FormImpl<P, A, I, ER, EW>
const path = [...form.path, index] as const
return new FormImpl(
path,
Subscribable.mapOptionEffect(form.value, Array.get(index)),
Lens.focusArrayAt(form.encodedValue, index),
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
form.isValidating,
form.canCommit,
form.isCommitting,
)
})
export const focusTupleAt: {
<P extends readonly PropertyKey[], A extends readonly [any, ...any[]], I extends readonly [any, ...any[]], ER, EW, K extends number>(
self: Form<P, A, I, ER, EW>,
index: K,
): Form<readonly [...P, K], A[K], I[K], ER, EW>
<P extends readonly PropertyKey[], A extends readonly [any, ...any[]], I extends readonly [any, ...any[]], ER, EW, K extends number>(
index: K,
): (self: Form<P, A, I, ER, EW>) => Form<readonly [...P, K], A[K], I[K], ER, EW>
} = Function.dual(2, <P extends readonly PropertyKey[], A extends readonly [any, ...any[]], I extends readonly [any, ...any[]], ER, EW, K extends number>(
self: Form<P, A, I, ER, EW>,
index: K,
): Form<readonly [...P, K], A[K], I[K], ER, EW> => {
const form = self as FormImpl<P, A, I, ER, EW>
const path = [...form.path, index] as const
return new FormImpl(
path,
Subscribable.mapOption(form.value, Array.unsafeGet(index)),
Lens.focusTupleAt(form.encodedValue, index),
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
form.isValidating,
form.canCommit,
form.isCommitting,
)
})
export const focusChunkAt: {
<P extends readonly PropertyKey[], A, I, ER, EW>(
self: Form<P, Chunk.Chunk<A>, Chunk.Chunk<I>, ER, EW>,
index: number,
): Form<readonly [...P, number], A, I, ER | Cause.NoSuchElementException, EW>
<P extends readonly PropertyKey[], A, I, ER, EW>(
index: number,
): (self: Form<P, Chunk.Chunk<A>, Chunk.Chunk<I>, ER, EW>) => Form<readonly [...P, number], A, I, ER | Cause.NoSuchElementException, EW>
} = Function.dual(2, <P extends readonly PropertyKey[], A, I, ER, EW>(
self: Form<P, Chunk.Chunk<A>, Chunk.Chunk<I>, ER, EW>,
index: number,
): Form<readonly [...P, number], A, I, ER | Cause.NoSuchElementException, EW> => {
const form = self as FormImpl<P, Chunk.Chunk<A>, Chunk.Chunk<I>, ER, EW>
const path = [...form.path, index] as const
return new FormImpl(
path,
Subscribable.mapOptionEffect(form.value, Chunk.get(index)),
Lens.focusChunkAt(form.encodedValue, index),
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
form.isValidating,
form.canCommit,
form.isCommitting,
)
})
export namespace useInput { export namespace useInput {
@@ -538,39 +301,33 @@ export namespace useInput {
} }
} }
export const useInput = Effect.fnUntraced(function* <P extends readonly PropertyKey[], A, I, ER, EW>( export const useInput = Effect.fnUntraced(function* <A, I>(
form: Form<P, A, I, ER, EW>, field: FormField<A, I>,
options?: useInput.Options, options?: useInput.Options,
): Effect.fn.Return<useInput.Success<I>, ER, Scope.Scope> { ): Effect.fn.Return<useInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> {
const internalValueLens = yield* Component.useOnChange(() => Effect.gen(function*() { const internalValueRef = yield* Component.useOnChange(() => Effect.tap(
const internalValueLens = yield* Lens.get(form.encodedValue).pipe( Effect.andThen(field.encodedValue, SubscriptionRef.make),
Effect.flatMap(SubscriptionRef.make), internalValueRef => Effect.forkScoped(Effect.all([
Effect.map(Lens.fromSubscriptionRef),
)
yield* Effect.forkScoped(Effect.all([
Stream.runForEach( Stream.runForEach(
Stream.drop(form.encodedValue.changes, 1), Stream.drop(field.encodedValue, 1),
upstreamEncodedValue => Effect.whenEffect( upstreamEncodedValue => Effect.whenEffect(
Lens.set(internalValueLens, upstreamEncodedValue), Ref.set(internalValueRef, upstreamEncodedValue),
Effect.andThen(Lens.get(internalValueLens), internalValue => !Equal.equals(upstreamEncodedValue, internalValue)), Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)),
), ),
), ),
Stream.runForEach( Stream.runForEach(
internalValueLens.changes.pipe( internalValueRef.changes.pipe(
Stream.drop(1), Stream.drop(1),
Stream.changesWith(Equal.equivalence()), Stream.changesWith(Equal.equivalence()),
options?.debounce ? Stream.debounce(options.debounce) : identity, options?.debounce ? Stream.debounce(options.debounce) : identity,
), ),
internalValue => Lens.set(form.encodedValue, internalValue), internalValue => Ref.set(field.encodedValue, internalValue),
), ),
], { concurrency: "unbounded" })) ], { concurrency: "unbounded" })),
), [field, options?.debounce])
return internalValueLens const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef)
}), [form, options?.debounce])
const [value, setValue] = yield* Lens.useState(internalValueLens)
return { value, setValue } return { value, setValue }
}) })
@@ -585,63 +342,55 @@ export namespace useOptionalInput {
} }
} }
export const useOptionalInput = Effect.fnUntraced(function* <P extends readonly PropertyKey[], A, I, ER, EW>( export const useOptionalInput = Effect.fnUntraced(function* <A, I>(
field: Form<P, A, Option.Option<I>, ER, EW>, field: FormField<A, Option.Option<I>>,
options: useOptionalInput.Options<I>, options: useOptionalInput.Options<I>,
): Effect.fn.Return<useOptionalInput.Success<I>, ER, Scope.Scope> { ): Effect.fn.Return<useOptionalInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> {
const [enabledLens, internalValueLens] = yield* Component.useOnChange(() => Effect.gen(function*() { const [enabledRef, internalValueRef] = yield* Component.useOnChange(() => Effect.tap(
const [enabledLens, internalValueLens] = yield* Effect.flatMap( Effect.andThen(
Lens.get(field.encodedValue), field.encodedValue,
Option.match({ Option.match({
onSome: v => Effect.all([ onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]),
Effect.map(SubscriptionRef.make(true), Lens.fromSubscriptionRef), onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]),
Effect.map(SubscriptionRef.make(v), Lens.fromSubscriptionRef),
]),
onNone: () => Effect.all([
Effect.map(SubscriptionRef.make(false), Lens.fromSubscriptionRef),
Effect.map(SubscriptionRef.make(options.defaultValue), Lens.fromSubscriptionRef),
]),
}), }),
) ),
yield* Effect.forkScoped(Effect.all([ ([enabledRef, internalValueRef]) => Effect.forkScoped(Effect.all([
Stream.runForEach( Stream.runForEach(
Stream.drop(field.encodedValue.changes, 1), Stream.drop(field.encodedValue, 1),
upstreamEncodedValue => Effect.whenEffect( upstreamEncodedValue => Effect.whenEffect(
Option.match(upstreamEncodedValue, { Option.match(upstreamEncodedValue, {
onSome: v => Effect.andThen( onSome: v => Effect.andThen(
Lens.set(enabledLens, true), Ref.set(enabledRef, true),
Lens.set(internalValueLens, v), Ref.set(internalValueRef, v),
), ),
onNone: () => Effect.andThen( onNone: () => Effect.andThen(
Lens.set(enabledLens, false), Ref.set(enabledRef, false),
Lens.set(internalValueLens, options.defaultValue), Ref.set(internalValueRef, options.defaultValue),
), ),
}), }),
Effect.andThen( Effect.andThen(
Effect.all([Lens.get(enabledLens), Lens.get(internalValueLens)]), Effect.all([enabledRef, internalValueRef]),
([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()), ([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()),
), ),
), ),
), ),
Stream.runForEach( Stream.runForEach(
enabledLens.changes.pipe( enabledRef.changes.pipe(
Stream.zipLatest(internalValueLens.changes), Stream.zipLatest(internalValueRef.changes),
Stream.drop(1), Stream.drop(1),
Stream.changesWith(Equal.equivalence()), Stream.changesWith(Equal.equivalence()),
options?.debounce ? Stream.debounce(options.debounce) : identity, options?.debounce ? Stream.debounce(options.debounce) : identity,
), ),
([enabled, internalValue]) => Lens.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()), ([enabled, internalValue]) => Ref.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()),
), ),
], { concurrency: "unbounded" })) ], { concurrency: "unbounded" })),
), [field, options.debounce])
return [enabledLens, internalValueLens] as const const [enabled, setEnabled] = yield* SubscriptionRef.useSubscriptionRefState(enabledRef)
}), [field, options.debounce]) const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef)
const [enabled, setEnabled] = yield* Lens.useState(enabledLens)
const [value, setValue] = yield* Lens.useState(internalValueLens)
return { enabled, setEnabled, value, setValue } return { enabled, setEnabled, value, setValue }
}) })

View File

@@ -1,63 +0,0 @@
import { Effect, Equivalence, Stream } from "effect"
import { Lens } from "effect-lens"
import * as React from "react"
import * as Component from "./Component.js"
import * as SetStateAction from "./SetStateAction.js"
import * as SubscriptionRef from "./SubscriptionRef.js"
export * from "effect-lens/Lens"
export declare namespace useState {
export interface Options<A> {
readonly equivalence?: Equivalence.Equivalence<A>
}
}
export const useState = Effect.fnUntraced(function* <A, ER, EW, RR, RW>(
lens: Lens.Lens<A, ER, EW, RR, RW>,
options?: useState.Options<NoInfer<A>>,
): Effect.fn.Return<readonly [A, React.Dispatch<React.SetStateAction<A>>], ER, RR | RW> {
const [reactStateValue, setReactStateValue] = React.useState(yield* Component.useOnMount(() => Lens.get(lens)))
yield* Component.useReactEffect(() => Effect.forkScoped(
Stream.runForEach(
Stream.changesWith(lens.changes, options?.equivalence ?? Equivalence.strict()),
v => Effect.sync(() => setReactStateValue(v)),
)
), [lens])
const setValue = yield* Component.useCallbackSync(
(setStateAction: React.SetStateAction<A>) => Effect.andThen(
Lens.updateAndGet(lens, prevState => SetStateAction.value(setStateAction, prevState)),
v => setReactStateValue(v),
),
[lens],
)
return [reactStateValue, setValue]
})
export declare namespace useFromState {
export interface Options<A> {
readonly equivalence?: Equivalence.Equivalence<A>
}
}
export const useFromState = Effect.fnUntraced(function* <A>(
[value, setValue]: readonly [A, React.Dispatch<React.SetStateAction<A>>],
options?: useFromState.Options<NoInfer<A>>,
): Effect.fn.Return<Lens.Lens<A, never, never, never, never>> {
const lens = yield* Component.useOnMount(() => Effect.map(
SubscriptionRef.make(value),
Lens.fromSubscriptionRef,
))
yield* Component.useReactEffect(() => Effect.forkScoped(Stream.runForEach(
Stream.changesWith(lens.changes, options?.equivalence ?? Equivalence.strict()),
v => Effect.sync(() => setValue(v)),
)), [setValue])
yield* Component.useReactEffect(() => Lens.set(lens, value), [value])
return lens
})

View File

@@ -111,12 +111,12 @@ export declare namespace make {
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, P = never>(
options: make.Options<K, A, E, R, P> options: make.Options<K, A, E, R, P>
): Effect.fn.Return< ): Effect.fn.Return<
Mutation<K, A, E, Result.forkEffect.OutputContext<R, P>, P>, Mutation<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
never, never,
Scope.Scope | Result.forkEffect.OutputContext<R, P> Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>
> { > {
return new MutationImpl( return new MutationImpl(
yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<R, P>>(), yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>>(),
options.f as any, options.f as any,
options.initialProgress as P, options.initialProgress as P,

View File

@@ -1,67 +0,0 @@
import { describe, expect, test } from "bun:test"
import { Option } from "effect"
import * as PropertyPath from "./PropertyPath.js"
describe("immutableSet with arrays", () => {
test("sets a top-level array element", () => {
const arr = [1, 2, 3]
const result = PropertyPath.immutableSet(arr, [1], 99)
expect(result).toEqual(Option.some([1, 99, 3]))
})
test("does not mutate the original array", () => {
const arr = [1, 2, 3]
PropertyPath.immutableSet(arr, [0], 42)
expect(arr).toEqual([1, 2, 3])
})
test("sets the first element of an array", () => {
const arr = ["a", "b", "c"]
const result = PropertyPath.immutableSet(arr, [0], "z")
expect(result).toEqual(Option.some(["z", "b", "c"]))
})
test("sets the last element of an array", () => {
const arr = [10, 20, 30]
const result = PropertyPath.immutableSet(arr, [2], 99)
expect(result).toEqual(Option.some([10, 20, 99]))
})
test("sets a nested array element inside an object", () => {
const obj = { tags: ["foo", "bar", "baz"] }
const result = PropertyPath.immutableSet(obj, ["tags", 1], "qux")
expect(result).toEqual(Option.some({ tags: ["foo", "qux", "baz"] }))
})
test("sets a deeply nested value inside an array of objects", () => {
const obj = { items: [{ name: "alice" }, { name: "bob" }] }
const result = PropertyPath.immutableSet(obj, ["items", 0, "name"], "charlie")
expect(result).toEqual(Option.some({ items: [{ name: "charlie" }, { name: "bob" }] }))
})
test("sets a value in a nested array", () => {
const matrix = [[1, 2], [3, 4]]
const result = PropertyPath.immutableSet(matrix, [1, 0], 99)
expect(result).toEqual(Option.some([[1, 2], [99, 4]]))
})
test("returns Option.none() for an out-of-bounds index", () => {
const arr = [1, 2, 3]
const result = PropertyPath.immutableSet(arr, [5], 99)
expect(result).toEqual(Option.none())
})
test("returns Option.none() for a non-numeric key on an array", () => {
const arr = [1, 2, 3]
// @ts-expect-error intentionally wrong key type
const result = PropertyPath.immutableSet(arr, ["length"], 0)
expect(result).toEqual(Option.none())
})
test("empty path returns Option.some of the value itself", () => {
const arr = [1, 2, 3]
const result = PropertyPath.immutableSet(arr, [], [9, 9, 9] as any)
expect(result).toEqual(Option.some([9, 9, 9]))
})
})

View File

@@ -281,14 +281,14 @@ export declare namespace make {
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 extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>(
options: make.Options<K, A, KE, KR, E, R, P> options: make.Options<K, A, KE, KR, E, R, P>
): Effect.fn.Return< ): Effect.fn.Return<
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<R, P>, P>, Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
never, never,
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<R, P> Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>
> { > {
const client = yield* QueryClient.QueryClient const client = yield* QueryClient.QueryClient
return new QueryImpl<K, A, KE, KR, E, Result.forkEffect.OutputContext<R, P>, P>( return new QueryImpl<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>(
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<R, P>>(), yield* Effect.context<Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>>(),
options.key, options.key,
options.f as any, options.f as any,
options.initialProgress as P, options.initialProgress as P,
@@ -308,9 +308,9 @@ export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, KE =
export const service = <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>( export const service = <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>(
options: make.Options<K, A, KE, KR, E, R, P> options: make.Options<K, A, KE, KR, E, R, P>
): Effect.Effect< ): Effect.Effect<
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<R, P>, P>, Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
never, never,
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<R, P> Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>
> => Effect.tap( > => Effect.tap(
make(options), make(options),
query => Effect.forkScoped(query.run), query => Effect.forkScoped(query.run),

View File

@@ -1,5 +1,4 @@
import { Cause, Context, Data, Effect, Equal, Exit, type Fiber, Hash, Layer, Match, pipe, Pipeable, Predicate, PubSub, Ref, type Scope, Stream, type Subscribable, SynchronizedRef } from "effect" import { Cause, Context, Data, Effect, Equal, Exit, type Fiber, Hash, Layer, Match, Pipeable, Predicate, PubSub, pipe, Ref, type Scope, Stream, Subscribable } from "effect"
import { Lens } from "effect-lens"
export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result") export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
@@ -16,6 +15,10 @@ export type Final<A, E = never, P = never> = (Success<A> | Failure<E>) & ({} | F
export type Flags<P = never> = WillFetch | WillRefresh | Refreshing<P> export type Flags<P = never> = WillFetch | WillRefresh | Refreshing<P>
export declare namespace Result { export declare namespace Result {
export interface Prototype extends Pipeable.Pipeable, Equal.Equal {
readonly [ResultTypeId]: ResultTypeId
}
export type Success<R extends Result<any, any, any>> = [R] extends [Result<infer A, infer _E, infer _P>] ? A : never export type Success<R extends Result<any, any, any>> = [R] extends [Result<infer A, infer _E, infer _P>] ? A : never
export type Failure<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer E, infer _P>] ? E : never export type Failure<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer E, infer _P>] ? E : never
export type Progress<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer _E, infer P>] ? P : never export type Progress<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer _E, infer P>] ? P : never
@@ -25,21 +28,21 @@ export declare namespace Flags {
export type Keys = keyof WillFetch & WillRefresh & Refreshing<any> export type Keys = keyof WillFetch & WillRefresh & Refreshing<any>
} }
export interface Initial extends ResultPrototype { export interface Initial extends Result.Prototype {
readonly _tag: "Initial" readonly _tag: "Initial"
} }
export interface Running<P = never> extends ResultPrototype { export interface Running<P = never> extends Result.Prototype {
readonly _tag: "Running" readonly _tag: "Running"
readonly progress: P readonly progress: P
} }
export interface Success<A> extends ResultPrototype { export interface Success<A> extends Result.Prototype {
readonly _tag: "Success" readonly _tag: "Success"
readonly value: A readonly value: A
} }
export interface Failure<E = never> extends ResultPrototype { export interface Failure<E = never> extends Result.Prototype {
readonly _tag: "Failure" readonly _tag: "Failure"
readonly cause: Cause.Cause<E> readonly cause: Cause.Cause<E>
} }
@@ -58,11 +61,7 @@ export interface Refreshing<P = never> {
} }
export interface ResultPrototype extends Pipeable.Pipeable, Equal.Equal { const ResultPrototype = Object.freeze({
readonly [ResultTypeId]: ResultTypeId
}
export const ResultPrototype: ResultPrototype = Object.freeze({
...Pipeable.Prototype, ...Pipeable.Prototype,
[ResultTypeId]: ResultTypeId, [ResultTypeId]: ResultTypeId,
@@ -96,7 +95,7 @@ export const ResultPrototype: ResultPrototype = Object.freeze({
Hash.cached(this), Hash.cached(this),
) )
}, },
} as const) } as const satisfies Result.Prototype)
export const isResult = (u: unknown): u is Result<unknown, unknown, unknown> => Predicate.hasProperty(u, ResultTypeId) export const isResult = (u: unknown): u is Result<unknown, unknown, unknown> => Predicate.hasProperty(u, ResultTypeId)
@@ -163,40 +162,52 @@ export const toExit: {
} }
export interface Progress<P = never> { export interface State<A, E = never, P = never> {
readonly progress: Lens.Lens<P, PreviousResultNotRunningNorRefreshing, never, never, never> readonly get: Effect.Effect<Result<A, E, P>>
readonly set: (v: Result<A, E, P>) => Effect.Effect<void>
}
export const State = <A, E = never, P = never>(): Context.Tag<State<A, E, P>, State<A, E, P>> => Context.GenericTag("@effect-fc/Result/State")
export interface Progress<P = never> {
readonly update: <E, R>(
f: (previous: P) => Effect.Effect<P, E, R>
) => Effect.Effect<void, PreviousResultNotRunningNorRefreshing | E, R>
} }
export const Progress = <P = never>(): Context.Tag<Progress<P>, Progress<P>> => Context.GenericTag("@effect-fc/Result/Progress")
export class PreviousResultNotRunningNorRefreshing extends Data.TaggedError("@effect-fc/Result/PreviousResultNotRunningNorRefreshing")<{ export class PreviousResultNotRunningNorRefreshing extends Data.TaggedError("@effect-fc/Result/PreviousResultNotRunningNorRefreshing")<{
readonly previous: Result<unknown, unknown, unknown> readonly previous: Result<unknown, unknown, unknown>
}> {} }> {}
export const makeProgressLayer = <A, E, P = never>( export const Progress = <P = never>(): Context.Tag<Progress<P>, Progress<P>> => Context.GenericTag("@effect-fc/Result/Progress")
state: Lens.Lens<Result<A, E, P>, never, never, never, never>
): Layer.Layer<Progress<P> | Progress<never>, never, never> => Layer.succeed( export const makeProgressLayer = <A, E, P = never>(): Layer.Layer<
Progress<P>() as Context.Tag<Progress<P> | Progress<never>, Progress<P> | Progress<never>>, Progress<P>,
{ never,
progress: state.pipe( State<A, E, P>
Lens.mapEffect( > => Layer.effect(Progress<P>(), Effect.gen(function*() {
a => (isRunning(a) || hasRefreshingFlag(a)) const state = yield* State<A, E, P>()
? Effect.succeed(a)
: Effect.fail(new PreviousResultNotRunningNorRefreshing({ previous: a })), return {
(_, b) => Effect.succeed(b), update: <FE, FR>(f: (previous: P) => Effect.Effect<P, FE, FR>) => Effect.Do.pipe(
Effect.bind("previous", () => Effect.andThen(state.get, previous =>
(isRunning(previous) || hasRefreshingFlag(previous))
? Effect.succeed(previous)
: Effect.fail(new PreviousResultNotRunningNorRefreshing({ previous })),
)),
Effect.bind("progress", ({ previous }) => f(previous.progress)),
Effect.let("next", ({ previous, progress }) => isRunning(previous)
? running(progress)
: refreshing(previous, progress) as Final<A, E, P> & Refreshing<P>
), ),
Lens.map( Effect.andThen(({ next }) => state.set(next)),
a => a.progress,
(a, b) => isRunning(a)
? running(b)
: refreshing(a, b) as Final<A, E, P> & Refreshing<P>,
), ),
) }
}, }))
)
export namespace unsafeForkEffect { export namespace unsafeForkEffect {
export type OutputContext<R, P> = Exclude<R, Progress<P> | Progress<never>> export type OutputContext<A, E, R, P> = Exclude<R, State<A, E, P> | Progress<P> | Progress<never>>
export interface Options<A, E, P> { export interface Options<A, E, P> {
readonly initial?: Initial | Final<A, E, P> readonly initial?: Initial | Final<A, E, P>
@@ -204,56 +215,55 @@ export namespace unsafeForkEffect {
} }
} }
export const unsafeForkEffect = Effect.fnUntraced(function* <A, E, R, P = never>( export const unsafeForkEffect = <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.Effect<
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>], readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
never, never,
Scope.Scope | unsafeForkEffect.OutputContext<R, P> Scope.Scope | unsafeForkEffect.OutputContext<A, E, R, P>
> { > => Effect.Do.pipe(
const ref = yield* SynchronizedRef.make<Result<A, E, P>>(options?.initial ?? initial<A, E, P>()) Effect.bind("ref", () => Ref.make(options?.initial ?? initial<A, E, P>())),
const pubsub = yield* PubSub.unbounded<Result<A, E, P>>() Effect.bind("pubsub", () => PubSub.unbounded<Result<A, E, P>>()),
Effect.bind("fiber", ({ ref, pubsub }) => Effect.forkScoped(State<A, E, P>().pipe(
const state = Lens.make<Result<A, E, P>, never, never, never, never>({ Effect.andThen(state => state.set(
get get() { return Ref.get(ref) },
get changes() {
return Stream.unwrapScoped(Effect.map(
Effect.all([Ref.get(ref), Stream.fromPubSub(pubsub, { scoped: true })]),
([latest, stream]) => Stream.concat(Stream.make(latest), stream),
))
},
modify: f => Ref.get(ref).pipe(
Effect.flatMap(f),
Effect.flatMap(([b, a]) => Ref.set(ref, a).pipe(
Effect.as(b),
Effect.zipLeft(PubSub.publish(pubsub, a))
)),
),
})
const fiber = yield* Effect.gen(function*() {
yield* Lens.set(
state,
(isFinal(options?.initial) && hasWillRefreshFlag(options?.initial)) (isFinal(options?.initial) && hasWillRefreshFlag(options?.initial))
? refreshing(options.initial, options?.initialProgress) as Result<A, E, P> ? refreshing(options.initial, options?.initialProgress) as Result<A, E, P>
: running(options?.initialProgress), : running(options?.initialProgress)
) ).pipe(
return yield* Effect.onExit(effect, exit => Effect.andThen( Effect.andThen(effect),
Lens.set(state, fromExit(exit)), Effect.onExit(exit => Effect.andThen(
state.set(fromExit(exit)),
Effect.forkScoped(PubSub.shutdown(pubsub)), Effect.forkScoped(PubSub.shutdown(pubsub)),
)) )),
}).pipe( )),
Effect.forkScoped, Effect.provide(Layer.empty.pipe(
Effect.provide(makeProgressLayer(state)), Layer.provideMerge(makeProgressLayer<A, E, P>()),
) Layer.provideMerge(Layer.succeed(State<A, E, P>(), {
get: Ref.get(ref),
return [state, fiber] as const set: v => Effect.andThen(Ref.set(ref, v), PubSub.publish(pubsub, v))
}) })),
)),
))),
Effect.map(({ ref, pubsub, fiber }) => [
Subscribable.make({
get: Ref.get(ref),
changes: Stream.unwrapScoped(Effect.map(
Effect.all([Ref.get(ref), Stream.fromPubSub(pubsub, { scoped: true })]),
([latest, stream]) => Stream.concat(Stream.make(latest), stream),
)),
}),
fiber,
]),
) as Effect.Effect<
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
never,
Scope.Scope | unsafeForkEffect.OutputContext<A, E, R, P>
>
export namespace forkEffect { export namespace forkEffect {
export type InputContext<R, P> = R extends Progress<infer X> ? [X] extends [P] ? R : never : R export type InputContext<R, P> = R extends Progress<infer X> ? [X] extends [P] ? R : never : R
export type OutputContext<R, P> = unsafeForkEffect.OutputContext<R, P> export type OutputContext<A, E, R, P> = unsafeForkEffect.OutputContext<A, E, R, P>
export interface Options<A, E, P> extends unsafeForkEffect.Options<A, E, P> {} export interface Options<A, E, P> extends unsafeForkEffect.Options<A, E, P> {}
} }
@@ -264,6 +274,6 @@ export const forkEffect: {
): Effect.Effect< ): Effect.Effect<
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>], readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
never, never,
Scope.Scope | forkEffect.OutputContext<R, P> Scope.Scope | forkEffect.OutputContext<A, E, R, P>
> >
} = unsafeForkEffect } = unsafeForkEffect

View File

@@ -3,8 +3,8 @@ import type * as React from "react"
export const value: { export const value: {
<S>(self: React.SetStateAction<S>, prevState: S): S
<S>(prevState: S): (self: React.SetStateAction<S>) => S <S>(prevState: S): (self: React.SetStateAction<S>) => S
<S>(self: React.SetStateAction<S>, prevState: S): S
} = Function.dual(2, <S>(self: React.SetStateAction<S>, prevState: S): S => } = Function.dual(2, <S>(self: React.SetStateAction<S>, prevState: S): S =>
typeof self === "function" typeof self === "function"
? (self as (prevState: S) => S)(prevState) ? (self as (prevState: S) => S)(prevState)

View File

@@ -1,11 +1,8 @@
import { Effect, Equivalence, Stream } from "effect" import { Effect, Equivalence, Stream, Subscribable } from "effect"
import { Subscribable } from "effect-lens"
import * as React from "react" import * as React from "react"
import * as Component from "./Component.js" import * as Component from "./Component.js"
export * from "effect-lens/Subscribable"
export const zipLatestAll = <const T extends readonly Subscribable.Subscribable<any, any, any>[]>( export const zipLatestAll = <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
...elements: T ...elements: T
): Subscribable.Subscribable< ): Subscribable.Subscribable<
@@ -51,3 +48,5 @@ export const useSubscribables = Effect.fnUntraced(function* <const T extends rea
return reactStateValue as any return reactStateValue as any
}) })
export * from "effect/Subscribable"

View File

@@ -1,183 +0,0 @@
import { describe, expect, test } from "bun:test"
import { Chunk, Effect, Ref, SubscriptionRef } from "effect"
import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
describe("SubscriptionSubRef with array refs", () => {
test("creates a subref for a single array element using path", async () => {
const value = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make([{ name: "alice" }, { name: "bob" }, { name: "charlie" }]),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [1, "name"])
return subref.get
},
),
)
expect(value).toBe("bob")
})
test("modifies a single array element via subref", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make([{ name: "alice" }, { name: "bob" }, { name: "charlie" }]),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [1, "name"])
return Effect.flatMap(
Ref.set(subref, "bob-updated"),
() => Ref.get(parent),
)
},
),
)
expect(result).toEqual([{ name: "alice" }, { name: "bob-updated" }, { name: "charlie" }])
})
test("modifies array element at index 0", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make([10, 20, 30]),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [0])
return Effect.flatMap(
Ref.set(subref, 99),
() => Ref.get(parent),
)
},
),
)
expect(result).toEqual([99, 20, 30])
})
test("modifies array element at last index", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make(["a", "b", "c"]),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [2])
return Effect.flatMap(
Ref.set(subref, "z"),
() => Ref.get(parent),
)
},
),
)
expect(result).toEqual(["a", "b", "z"])
})
test("modifies nested array element", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make([[1, 2], [3, 4], [5, 6]]),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [1, 0])
return Effect.flatMap(
Ref.set(subref, 99),
() => Ref.get(parent),
)
},
),
)
expect(result).toEqual([[1, 2], [99, 4], [5, 6]])
})
test("uses modifyEffect to transform array element", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make([{ count: 1 }, { count: 2 }, { count: 3 }]),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [1, "count"])
return Effect.flatMap(
Ref.update(subref, count => count + 100),
() => Effect.map(Ref.get(parent), parentValue => ({ result: 102, parentValue })),
)
},
),
)
expect(result.result).toBe(102) // count + 100
expect(result.parentValue).toEqual([{ count: 1 }, { count: 102 }, { count: 3 }]) // count + 100
})
test("uses modify to transform array element", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make([10, 20, 30]),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [1])
return Effect.flatMap(
Ref.update(subref, x => x + 5),
() => Effect.map(Ref.get(parent), parentValue => ({ result: 25, parentValue })),
)
},
),
)
expect(result.result).toBe(25) // 20 + 5
expect(result.parentValue).toEqual([10, 25, 30]) // 20 + 5
})
test("makeFromChunkIndex modifies chunk element", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make(Chunk.make(100, 200, 300)),
parent => {
const subref = SubscriptionSubRef.makeFromChunkIndex(parent, 1)
return Effect.flatMap(
Ref.set(subref, 999),
() => Ref.get(parent),
)
},
),
)
expect(Chunk.toReadonlyArray(result)).toEqual([100, 999, 300])
})
test("makeFromGetSet with custom getter/setter for array element", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make([{ id: 1, value: "a" }, { id: 2, value: "b" }]),
parent => {
const subref = SubscriptionSubRef.makeFromGetSet(parent, {
get: arr => arr[0].value,
set: (arr, newValue) => [
{ ...arr[0], value: newValue },
...arr.slice(1),
],
})
return Effect.flatMap(
Ref.set(subref, "updated"),
() => Ref.get(parent),
)
},
),
)
expect(result).toEqual([{ id: 1, value: "updated" }, { id: 2, value: "b" }])
})
test("does not mutate original array when modifying via subref", async () => {
const original = [{ name: "alice" }, { name: "bob" }]
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make(original),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [0, "name"])
return Effect.flatMap(
Ref.set(subref, "alice-updated"),
() => Ref.get(parent),
)
},
),
)
expect(original).toEqual([{ name: "alice" }, { name: "bob" }]) // original unchanged
expect(result).toEqual([{ name: "alice-updated" }, { name: "bob" }]) // new value in ref
})
})

View File

@@ -2,7 +2,6 @@ export * as Async from "./Async.js"
export * as Component from "./Component.js" export * as Component from "./Component.js"
export * as ErrorObserver from "./ErrorObserver.js" export * as ErrorObserver from "./ErrorObserver.js"
export * as Form from "./Form.js" export * as Form from "./Form.js"
export * as Lens from "./Lens.js"
export * as Memoized from "./Memoized.js" export * as Memoized from "./Memoized.js"
export * as Mutation from "./Mutation.js" export * as Mutation from "./Mutation.js"
export * as PropertyPath from "./PropertyPath.js" export * as PropertyPath from "./PropertyPath.js"

View File

@@ -25,7 +25,6 @@
"noPropertyAccessFromIndexSignature": false, "noPropertyAccessFromIndexSignature": false,
// Build // Build
"rootDir": "./src",
"outDir": "./dist", "outDir": "./dist",
"declaration": true, "declaration": true,
"sourceMap": true, "sourceMap": true,
@@ -35,6 +34,5 @@
] ]
}, },
"include": ["./src"], "include": ["./src"]
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
} }

View File

@@ -13,30 +13,30 @@
"clean:modules": "rm -rf node_modules" "clean:modules": "rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"@tanstack/react-router": "^1.168.3", "@tanstack/react-router": "^1.154.12",
"@tanstack/react-router-devtools": "^1.166.11", "@tanstack/react-router-devtools": "^1.154.12",
"@tanstack/router-plugin": "^1.167.4", "@tanstack/router-plugin": "^1.154.12",
"@types/react": "^19.2.14", "@types/react": "^19.2.9",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^5.1.2",
"globals": "^17.4.0", "globals": "^17.0.0",
"react": "^19.2.4", "react": "^19.2.3",
"react-dom": "^19.2.4", "react-dom": "^19.2.3",
"type-fest": "^5.5.0", "type-fest": "^5.4.1",
"vite": "^8.0.2" "vite": "^7.3.1"
}, },
"dependencies": { "dependencies": {
"@effect/platform": "^0.96.0", "@effect/platform": "^0.94.2",
"@effect/platform-browser": "^0.76.0", "@effect/platform-browser": "^0.74.0",
"@radix-ui/themes": "^3.3.0", "@radix-ui/themes": "^3.2.1",
"@typed/id": "^0.17.2", "@typed/id": "^0.17.2",
"effect": "^3.21.0", "effect": "^3.19.15",
"effect-fc": "workspace:*", "effect-fc": "workspace:*",
"react-icons": "^5.6.0" "react-icons": "^5.5.0"
}, },
"overrides": { "overrides": {
"@types/react": "^19.2.14", "@types/react": "^19.2.9",
"effect": "^3.21.0", "effect": "^3.19.15",
"react": "^19.2.4" "react": "^19.2.3"
} }
} }

View File

@@ -1,22 +1,24 @@
import { Callout, Flex, Spinner, TextField } from "@radix-ui/themes" import { Callout, Flex, Spinner, TextField } from "@radix-ui/themes"
import { Array, Option, Struct } from "effect" import { Array, Option } from "effect"
import { Component, Form, Subscribable } from "effect-fc" import { Component, Form, Subscribable } from "effect-fc"
export declare namespace TextFieldFormInputView { export declare namespace TextFieldFormInputView {
export interface Props extends Omit<TextField.RootProps, "form">, Form.useInput.Options { export interface Props
readonly form: Form.Form<readonly PropertyKey[], any, string> extends TextField.RootProps, Form.useInput.Options {
readonly field: Form.FormField<any, string>
} }
} }
export class TextFieldFormInputView extends Component.make("TextFieldFormInputView")(function*( export class TextFieldFormInputView extends Component.make("TextFieldFormInputView")(function*(
props: TextFieldFormInputView.Props props: TextFieldFormInputView.Props
) { ) {
const input = yield* Form.useInput(props.form, props) const input = yield* Form.useInput(props.field, props)
const [issues, isValidating, isCommitting] = yield* Subscribable.useSubscribables([ const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
props.form.issues, props.field.issues,
props.form.isValidating, props.field.isValidating,
props.form.isCommitting, props.field.isSubmitting,
]) ])
return ( return (
@@ -24,8 +26,8 @@ export class TextFieldFormInputView extends Component.make("TextFieldFormInputVi
<TextField.Root <TextField.Root
value={input.value} value={input.value}
onChange={e => input.setValue(e.target.value)} onChange={e => input.setValue(e.target.value)}
disabled={isCommitting} disabled={isSubmitting}
{...Struct.omit(props, "form")} {...props}
> >
{isValidating && {isValidating &&
<TextField.Slot side="right"> <TextField.Slot side="right">

View File

@@ -4,19 +4,21 @@ import { Component, Form, Subscribable } from "effect-fc"
export declare namespace TextFieldOptionalFormInputView { export declare namespace TextFieldOptionalFormInputView {
export interface Props extends Omit<TextField.RootProps, "form" | "defaultValue">, Form.useOptionalInput.Options<string> { export interface Props
readonly form: Form.Form<readonly PropertyKey[], any, Option.Option<string>> extends Omit<TextField.RootProps, "defaultValue">, Form.useOptionalInput.Options<string> {
readonly field: Form.FormField<any, Option.Option<string>>
} }
} }
export class TextFieldOptionalFormInputView extends Component.make("TextFieldOptionalFormInputView")(function*( export class TextFieldOptionalFormInputView extends Component.make("TextFieldOptionalFormInputView")(function*(
props: TextFieldOptionalFormInputView.Props props: TextFieldOptionalFormInputView.Props
) { ) {
const input = yield* Form.useOptionalInput(props.form, props) const input = yield* Form.useOptionalInput(props.field, props)
const [issues, isValidating, isCommitting] = yield* Subscribable.useSubscribables([ const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
props.form.issues, props.field.issues,
props.form.isValidating, props.field.isValidating,
props.form.isCommitting, props.field.isSubmitting,
]) ])
return ( return (
@@ -24,8 +26,8 @@ export class TextFieldOptionalFormInputView extends Component.make("TextFieldOpt
<TextField.Root <TextField.Root
value={input.value} value={input.value}
onChange={e => input.setValue(e.target.value)} onChange={e => input.setValue(e.target.value)}
disabled={!input.enabled || isCommitting} disabled={!input.enabled || isSubmitting}
{...Struct.omit(props, "form", "defaultValue")} {...Struct.omit(props, "defaultValue")}
> >
<TextField.Slot side="left"> <TextField.Slot side="left">
<Switch <Switch

View File

@@ -1,11 +1,11 @@
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
import { runtime } from "@/runtime"
import { Button, Container, Flex, Text } from "@radix-ui/themes" import { Button, Container, Flex, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect" import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
import { Component, Form, Subscribable } from "effect-fc" import { Component, Form, Subscribable } from "effect-fc"
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
import { runtime } from "@/runtime"
const email = Schema.pattern<typeof Schema.String>( const email = Schema.pattern<typeof Schema.String>(
@@ -40,8 +40,7 @@ const RegisterFormSubmitSchema = Schema.Struct({
}) })
class RegisterFormService extends Effect.Service<RegisterFormService>()("RegisterFormService", { class RegisterFormService extends Effect.Service<RegisterFormService>()("RegisterFormService", {
scoped: Effect.gen(function*() { scoped: Form.service({
const form = yield* Form.service({
schema: RegisterFormSchema.pipe( schema: RegisterFormSchema.pipe(
Schema.compose( Schema.compose(
Schema.transformOrFail( Schema.transformOrFail(
@@ -60,22 +59,15 @@ class RegisterFormService extends Effect.Service<RegisterFormService>()("Registe
yield* Effect.sleep("500 millis") yield* Effect.sleep("500 millis")
return yield* Schema.decode(RegisterFormSubmitSchema)(value) return yield* Schema.decode(RegisterFormSubmitSchema)(value)
}), }),
}) debounce: "500 millis",
return {
form,
emailField: Form.focusObjectField(form, "email"),
passwordField: Form.focusObjectField(form, "password"),
birthField: Form.focusObjectField(form, "birth"),
} as const
}) })
}) {} }) {}
class RegisterFormView extends Component.make("RegisterFormView")(function*() { class RegisterFormView extends Component.make("RegisterFormView")(function*() {
const form = yield* RegisterFormService const form = yield* RegisterFormService
const [canCommit, submitResult] = yield* Subscribable.useSubscribables([ const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
form.form.canCommit, form.canSubmit,
form.form.mutation.result, form.mutation.result,
]) ])
const runPromise = yield* Component.useRunPromise() const runPromise = yield* Component.useRunPromise()
@@ -92,26 +84,24 @@ class RegisterFormView extends Component.make("RegisterFormView")(function*() {
<Container width="300"> <Container width="300">
<form onSubmit={e => { <form onSubmit={e => {
e.preventDefault() e.preventDefault()
void runPromise(form.form.submit) void runPromise(form.submit)
}}> }}>
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
<TextFieldFormInput <TextFieldFormInput
form={form.emailField} field={yield* form.field(["email"])}
debounce="250 millis"
/> />
<TextFieldFormInput <TextFieldFormInput
form={form.passwordField} field={yield* form.field(["password"])}
debounce="250 millis"
/> />
<TextFieldOptionalFormInput <TextFieldOptionalFormInput
type="datetime-local" type="datetime-local"
form={form.birthField} field={yield* form.field(["birth"])}
defaultValue="" defaultValue=""
/> />
<Button disabled={!canCommit}>Submit</Button> <Button disabled={!canSubmit}>Submit</Button>
</Flex> </Flex>
</form> </form>

View File

@@ -1,13 +1,13 @@
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Chunk, type DateTime, Effect, Match, Option, Ref, Schema, Stream } from "effect"
import { Component, Form, Subscribable } from "effect-fc"
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
import { FaDeleteLeft } from "react-icons/fa6"
import * as Domain from "@/domain" import * as Domain from "@/domain"
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView" import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView" import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
import { DateTimeUtcFromZonedInput } from "@/lib/schema" import { DateTimeUtcFromZonedInput } from "@/lib/schema"
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Chunk, type DateTime, Effect, Match, Option, Ref, Schema, Stream } from "effect"
import { Component, Form, Lens, Subscribable } from "effect-fc"
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
import { FaDeleteLeft } from "react-icons/fa6"
import { TodosState } from "./TodosState" import { TodosState } from "./TodosState"
@@ -59,25 +59,27 @@ export class TodoView extends Component.make("TodoView")(function*(props: TodoPr
Match.tag("new", () => Ref.update(state.ref, Chunk.prepend(todo)).pipe( Match.tag("new", () => Ref.update(state.ref, Chunk.prepend(todo)).pipe(
Effect.andThen(makeTodo), Effect.andThen(makeTodo),
Effect.andThen(Schema.encode(TodoFormSchema)), Effect.andThen(Schema.encode(TodoFormSchema)),
Effect.andThen(v => Lens.set(form.encodedValue, v)), Effect.andThen(v => Ref.set(form.encodedValue, v)),
)), )),
Match.tag("edit", ({ id }) => Ref.set(state.getElementRef(id), todo)), Match.tag("edit", ({ id }) => Ref.set(state.getElementRef(id), todo)),
Match.exhaustive, Match.exhaustive,
), ),
autosubmit: props._tag === "edit",
debounce: "250 millis",
}) })
return [ return [
indexRef, indexRef,
form, form,
Form.focusObjectField(form, "content"), yield* form.field(["content"]),
Form.focusObjectField(form, "completedAt"), yield* form.field(["completedAt"]),
] as const ] as const
}), [props._tag, props._tag === "edit" ? props.id : undefined]) }), [props._tag, props._tag === "edit" ? props.id : undefined])
const [index, size, canCommit] = yield* Subscribable.useSubscribables([ const [index, size, canSubmit] = yield* Subscribable.useSubscribables([
indexRef, indexRef,
state.sizeSubscribable, state.sizeSubscribable,
form.canCommit, form.canSubmit,
]) ])
const runSync = yield* Component.useRunSync() const runSync = yield* Component.useRunSync()
@@ -90,20 +92,17 @@ export class TodoView extends Component.make("TodoView")(function*(props: TodoPr
<Flex direction="row" align="center" gap="2"> <Flex direction="row" align="center" gap="2">
<Box flexGrow="1"> <Box flexGrow="1">
<Flex direction="column" align="stretch" gap="2"> <Flex direction="column" align="stretch" gap="2">
<TextFieldFormInput <TextFieldFormInput field={contentField} />
form={contentField}
debounce="250 millis"
/>
<Flex direction="row" justify="center" align="center" gap="2"> <Flex direction="row" justify="center" align="center" gap="2">
<TextFieldOptionalFormInput <TextFieldOptionalFormInput
form={completedAtField} field={completedAtField}
type="datetime-local" type="datetime-local"
defaultValue="" defaultValue=""
/> />
{props._tag === "new" && {props._tag === "new" &&
<Button disabled={!canCommit} onClick={() => void runPromise(form.submit)}> <Button disabled={!canSubmit} onClick={() => void runPromise(form.submit)}>
Add Add
</Button> </Button>
} }