Update dependency @effect/language-service to ^0.84.0 #39

Closed
renovate-bot wants to merge 65 commits from renovate/bun-minor-patch into next
11 changed files with 644 additions and 441 deletions

591
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.6", "packageManager": "bun@1.3.11",
"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.3.11", "@biomejs/biome": "^2.4.8",
"@effect/language-service": "^0.75.0", "@effect/language-service": "^0.84.0",
"@types/bun": "^1.3.6", "@types/bun": "^1.3.11",
"npm-check-updates": "^19.3.1", "npm-check-updates": "^19.6.5",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",
"turbo": "^2.7.5", "turbo": "^2.8.20",
"typescript": "^5.9.3" "typescript": "^6.0.2"
} }
} }

View File

@@ -27,7 +27,7 @@
"@docusaurus/module-type-aliases": "3.9.2", "@docusaurus/module-type-aliases": "3.9.2",
"@docusaurus/tsconfig": "3.9.2", "@docusaurus/tsconfig": "3.9.2",
"@docusaurus/types": "3.9.2", "@docusaurus/types": "3.9.2",
"typescript": "~5.6.2" "typescript": "~5.9.0"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

View File

@@ -38,11 +38,14 @@
"clean:modules": "rm -rf node_modules" "clean:modules": "rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"@effect/platform-browser": "^0.74.0" "@effect/platform-browser": "^0.76.0"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0", "@types/react": "^19.2.0",
"effect": "^3.19.0", "effect": "^3.21.0",
"react": "^19.2.0" "react": "^19.2.0"
},
"dependencies": {
"effect-lens": "^0.1.1"
} }
} }

View File

@@ -1,12 +1,12 @@
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 { 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 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")
@@ -26,7 +26,7 @@ extends Pipeable.Pipeable {
readonly debounce: Option.Option<Duration.DurationInput> readonly debounce: Option.Option<Duration.DurationInput>
readonly value: Subscribable.Subscribable<Option.Option<A>> readonly value: Subscribable.Subscribable<Option.Option<A>>
readonly encodedValue: SubscriptionRef.SubscriptionRef<I> readonly encodedValue: Lens.Lens<I>
readonly error: Subscribable.Subscribable<Option.Option<ParseResult.ParseError>> readonly error: Subscribable.Subscribable<Option.Option<ParseResult.ParseError>>
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>> readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>
@@ -54,10 +54,10 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
readonly autosubmit: boolean, readonly autosubmit: boolean,
readonly debounce: Option.Option<Duration.DurationInput>, readonly debounce: Option.Option<Duration.DurationInput>,
readonly value: SubscriptionRef.SubscriptionRef<Option.Option<A>>, readonly value: Lens.Lens<Option.Option<A>>,
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>, readonly encodedValue: Lens.Lens<I>,
readonly error: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>, readonly error: Lens.Lens<Option.Option<ParseResult.ParseError>>,
readonly validationFiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>, readonly validationFiber: Lens.Lens<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>>>, readonly fieldCache: Ref.Ref<HashMap.HashMap<FormFieldKey, FormField<unknown, unknown>>>,
@@ -99,7 +99,7 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
Option.isSome(this.debounce) ? Stream.debounce(this.debounce.value) : identity Option.isSome(this.debounce) ? Stream.debounce(this.debounce.value) : identity
), ),
encodedValue => this.validationFiber.pipe( encodedValue => Lens.get(this.validationFiber).pipe(
Effect.andThen(Option.match({ Effect.andThen(Option.match({
onSome: Fiber.interrupt, onSome: Fiber.interrupt,
onNone: () => Effect.void, onNone: () => Effect.void,
@@ -110,18 +110,18 @@ extends Pipeable.Class() implements Form<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(
Ref.set(this.value, Option.some(v)), Lens.set(this.value, Option.some(v)),
Ref.set(this.error, Option.none()), Lens.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 => Ref.set(this.error, Option.some(e)), onSome: e => Lens.set(this.error, Option.some(e)),
onNone: () => Effect.void, onNone: () => Effect.void,
}), }),
}), }),
Ref.set(this.validationFiber, Option.none()), Lens.set(this.validationFiber, Option.none()),
), ),
)).pipe( )).pipe(
Effect.tap(fiber => Ref.set(this.validationFiber, Option.some(fiber))), Effect.tap(fiber => Lens.set(this.validationFiber, Option.some(fiber))),
Effect.andThen(Fiber.join), Effect.andThen(Fiber.join),
Effect.andThen(value => this.autosubmit Effect.andThen(value => this.autosubmit
? Effect.asVoid(Effect.forkScoped(this.submitValue(value))) ? Effect.asVoid(Effect.forkScoped(this.submitValue(value)))
@@ -136,7 +136,7 @@ extends Pipeable.Class() implements Form<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 this.value.pipe( return Lens.get(this.value).pipe(
Effect.andThen(identity), Effect.andThen(identity),
Effect.andThen(value => this.submitValue(value)), Effect.andThen(value => this.submitValue(value)),
) )
@@ -153,7 +153,7 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
e => e._tag === "ParseError", e => e._tag === "ParseError",
), ),
{ {
onSome: e => Ref.set(this.error, Option.some(e)), onSome: e => Lens.set(this.error, Option.some(e)),
onNone: () => Effect.void, onNone: () => Effect.void,
}, },
) )
@@ -193,10 +193,10 @@ export const make = Effect.fnUntraced(function* <A, I = A, R = never, MA = void,
options.autosubmit ?? false, options.autosubmit ?? false,
Option.fromNullable(options.debounce), Option.fromNullable(options.debounce),
yield* SubscriptionRef.make(Option.none<A>()), Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<A>())),
yield* SubscriptionRef.make(options.initialEncodedValue), Lens.fromSubscriptionRef(yield* SubscriptionRef.make(options.initialEncodedValue)),
yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>()), Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>())),
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>()), Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>())),
yield* Effect.makeSemaphore(1), yield* Effect.makeSemaphore(1),
yield* Ref.make(HashMap.empty<FormFieldKey, FormField<unknown, unknown>>()), yield* Ref.make(HashMap.empty<FormFieldKey, FormField<unknown, unknown>>()),
@@ -228,7 +228,7 @@ extends Pipeable.Pipeable {
readonly [FormFieldTypeId]: FormFieldTypeId readonly [FormFieldTypeId]: FormFieldTypeId
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException> readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>
readonly encodedValue: SubscriptionRef.SubscriptionRef<I> readonly encodedValue: Lens.Lens<I>
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]> readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
readonly isValidating: Subscribable.Subscribable<boolean> readonly isValidating: Subscribable.Subscribable<boolean>
readonly isSubmitting: Subscribable.Subscribable<boolean> readonly isSubmitting: Subscribable.Subscribable<boolean>
@@ -240,7 +240,7 @@ extends Pipeable.Class() implements FormField<A, I> {
constructor( constructor(
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>, readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>,
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>, readonly encodedValue: Lens.Lens<I>,
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>, readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>,
readonly isValidating: Subscribable.Subscribable<boolean>, readonly isValidating: Subscribable.Subscribable<boolean>,
readonly isSubmitting: Subscribable.Subscribable<boolean>, readonly isSubmitting: Subscribable.Subscribable<boolean>,
@@ -276,7 +276,7 @@ export const makeFormField = <A, I, R, MA, ME, MR, MP, const P extends PropertyP
onSome: v => Option.map(PropertyPath.get(v, path), Option.some), onSome: v => Option.map(PropertyPath.get(v, path), Option.some),
onNone: () => Option.some(Option.none()), onNone: () => Option.some(Option.none()),
})), })),
SubscriptionSubRef.makeFromPath(self.encodedValue, path), Lens.map(self.encodedValue, a => Option.getOrThrow(PropertyPath.get(a, path)), (a, b) => Option.getOrThrow(PropertyPath.immutableSet(a, path, b))),
Subscribable.mapEffect(self.error, Option.match({ Subscribable.mapEffect(self.error, Option.match({
onSome: flow( onSome: flow(
ParseResult.ArrayFormatter.formatError, ParseResult.ArrayFormatter.formatError,
@@ -305,29 +305,35 @@ export const useInput = Effect.fnUntraced(function* <A, I>(
field: FormField<A, I>, field: FormField<A, I>,
options?: useInput.Options, options?: useInput.Options,
): Effect.fn.Return<useInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> { ): Effect.fn.Return<useInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> {
const internalValueRef = yield* Component.useOnChange(() => Effect.tap( const internalValueLens = yield* Component.useOnChange(() => Effect.gen(function*() {
Effect.andThen(field.encodedValue, SubscriptionRef.make), const internalValueLens = yield* Lens.get(field.encodedValue).pipe(
internalValueRef => Effect.forkScoped(Effect.all([ Effect.flatMap(SubscriptionRef.make),
Effect.map(Lens.fromSubscriptionRef),
)
yield* Effect.forkScoped(Effect.all([
Stream.runForEach( Stream.runForEach(
Stream.drop(field.encodedValue, 1), Stream.drop(field.encodedValue.changes, 1),
upstreamEncodedValue => Effect.whenEffect( upstreamEncodedValue => Effect.whenEffect(
Ref.set(internalValueRef, upstreamEncodedValue), Lens.set(internalValueLens, upstreamEncodedValue),
Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)), Effect.andThen(Lens.get(internalValueLens), internalValue => !Equal.equals(upstreamEncodedValue, internalValue)),
), ),
), ),
Stream.runForEach( Stream.runForEach(
internalValueRef.changes.pipe( internalValueLens.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 => Ref.set(field.encodedValue, internalValue), internalValue => Lens.set(field.encodedValue, internalValue),
), ),
], { concurrency: "unbounded" })), ], { concurrency: "unbounded" }))
), [field, options?.debounce])
const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef) return internalValueLens
}), [field, options?.debounce])
const [value, setValue] = yield* Lens.useState(internalValueLens)
return { value, setValue } return { value, setValue }
}) })
@@ -346,51 +352,59 @@ export const useOptionalInput = Effect.fnUntraced(function* <A, I>(
field: FormField<A, Option.Option<I>>, field: FormField<A, Option.Option<I>>,
options: useOptionalInput.Options<I>, options: useOptionalInput.Options<I>,
): Effect.fn.Return<useOptionalInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> { ): Effect.fn.Return<useOptionalInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> {
const [enabledRef, internalValueRef] = yield* Component.useOnChange(() => Effect.tap( const [enabledLens, internalValueLens] = yield* Component.useOnChange(() => Effect.gen(function*() {
Effect.andThen( const [enabledLens, internalValueLens] = yield* Effect.flatMap(
field.encodedValue, Lens.get(field.encodedValue),
Option.match({ Option.match({
onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]), onSome: v => Effect.all([
onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]), Effect.map(SubscriptionRef.make(true), Lens.fromSubscriptionRef),
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),
]),
}), }),
), )
([enabledRef, internalValueRef]) => Effect.forkScoped(Effect.all([ yield* Effect.forkScoped(Effect.all([
Stream.runForEach( Stream.runForEach(
Stream.drop(field.encodedValue, 1), Stream.drop(field.encodedValue.changes, 1),
upstreamEncodedValue => Effect.whenEffect( upstreamEncodedValue => Effect.whenEffect(
Option.match(upstreamEncodedValue, { Option.match(upstreamEncodedValue, {
onSome: v => Effect.andThen( onSome: v => Effect.andThen(
Ref.set(enabledRef, true), Lens.set(enabledLens, true),
Ref.set(internalValueRef, v), Lens.set(internalValueLens, v),
), ),
onNone: () => Effect.andThen( onNone: () => Effect.andThen(
Ref.set(enabledRef, false), Lens.set(enabledLens, false),
Ref.set(internalValueRef, options.defaultValue), Lens.set(internalValueLens, options.defaultValue),
), ),
}), }),
Effect.andThen( Effect.andThen(
Effect.all([enabledRef, internalValueRef]), Effect.all([Lens.get(enabledLens), Lens.get(internalValueLens)]),
([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(
enabledRef.changes.pipe( enabledLens.changes.pipe(
Stream.zipLatest(internalValueRef.changes), Stream.zipLatest(internalValueLens.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]) => Ref.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()), ([enabled, internalValue]) => Lens.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()),
), ),
], { concurrency: "unbounded" })), ], { concurrency: "unbounded" }))
), [field, options.debounce])
const [enabled, setEnabled] = yield* SubscriptionRef.useSubscriptionRefState(enabledRef) return [enabledLens, internalValueLens] as const
const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef) }), [field, options.debounce])
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

@@ -0,0 +1,63 @@
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 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
})
export * from "effect-lens/Lens"

View File

@@ -0,0 +1,67 @@
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

@@ -0,0 +1,183 @@
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,6 +2,7 @@ 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

@@ -34,5 +34,6 @@
] ]
}, },
"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.154.12", "@tanstack/react-router": "^1.168.3",
"@tanstack/react-router-devtools": "^1.154.12", "@tanstack/react-router-devtools": "^1.166.11",
"@tanstack/router-plugin": "^1.154.12", "@tanstack/router-plugin": "^1.167.4",
"@types/react": "^19.2.9", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^6.0.1",
"globals": "^17.0.0", "globals": "^17.4.0",
"react": "^19.2.3", "react": "^19.2.4",
"react-dom": "^19.2.3", "react-dom": "^19.2.4",
"type-fest": "^5.4.1", "type-fest": "^5.5.0",
"vite": "^7.3.1" "vite": "^8.0.2"
}, },
"dependencies": { "dependencies": {
"@effect/platform": "^0.94.2", "@effect/platform": "^0.96.0",
"@effect/platform-browser": "^0.74.0", "@effect/platform-browser": "^0.76.0",
"@radix-ui/themes": "^3.2.1", "@radix-ui/themes": "^3.3.0",
"@typed/id": "^0.17.2", "@typed/id": "^0.17.2",
"effect": "^3.19.15", "effect": "^3.21.0",
"effect-fc": "workspace:*", "effect-fc": "workspace:*",
"react-icons": "^5.5.0" "react-icons": "^5.6.0"
}, },
"overrides": { "overrides": {
"@types/react": "^19.2.9", "@types/react": "^19.2.14",
"effect": "^3.19.15", "effect": "^3.21.0",
"react": "^19.2.3" "react": "^19.2.4"
} }
} }