Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fair-dodos-observe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@typeonce/effect-machine": patch
---

Fix `AtomMachine` selectors so machines and invoked children with declared emitted events retain typed state selection and matching after `make` or `bind`.
29 changes: 16 additions & 13 deletions src/internal/machine/atom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ const resumeMachineAtomEffect = (
snapshot: Machine.Machine.Snapshot<any>
) => runMachineAtomEffect(get, internalMachine.resume(machine as any, snapshot as any))

type RefState<Ref> = Ref extends Machine.MachineRef<infer State, any, any, any> ? State : never
type RefError<Ref> = Ref extends Machine.MachineRef<any, any, infer Error, any> ? Error : never
type RefOutput<Ref> = Ref extends Machine.MachineRef<any, any, any, infer Output> ? Output : never
type RefState<Ref> = Ref extends Machine.MachineRef<infer State, any, any, any, any> ? State : never
type RefError<Ref> = Ref extends Machine.MachineRef<any, any, infer Error, any, any> ? Error : never
type RefOutput<Ref> = Ref extends Machine.MachineRef<any, any, any, infer Output, any> ? Output : never
type RefEmitted<Ref> = Ref extends Machine.MachineRef<any, any, any, any, infer Emitted> ? Emitted : never

export const emissions = <State, Event, Error, Output, StartError, Emitted>(
Expand Down Expand Up @@ -235,7 +235,7 @@ const makeChildRuntimeResultAtom = <State, Error, Output, StartError>(

const makeChildRefAtom = <Child extends Machine.ChildMachine.Any, StartError>(
parentRef: Atom.Atom<
AsyncResult.AsyncResult<Option.Option<Machine.MachineRef<any, any, any, any>>, StartError>
AsyncResult.AsyncResult<Option.Option<Machine.MachineRef<any, any, any, any, any>>, StartError>
>,
child: Child
): Atom.Atom<AsyncResult.AsyncResult<Option.Option<Machine.ChildMachine.Ref<Child>>, StartError>> =>
Expand Down Expand Up @@ -292,7 +292,7 @@ const makeChildFromRefAtom = <Child extends Machine.ChildMachine.Any, StartError
return AsyncResult.success(Option.none())
}

const handle = result.value.value as unknown as Machine.MachineRef<State, any, Error, Output>
const handle = result.value.value as unknown as Machine.MachineRef<State, any, Error, Output, any>
const cancel = Effect.runCallback(
handle.changes.pipe(
Stream.runForEach((snapshot) => Effect.sync(() => get.setSelf(AsyncResult.success(Option.some(snapshot)))))
Expand Down Expand Up @@ -363,9 +363,9 @@ const makeChildFromRefAtom = <Child extends Machine.ChildMachine.Any, StartError
}
}

const makeFromRefAtom = <State, Event, Error, Output, StartError>(
ref: Atom.Atom<AsyncResult.AsyncResult<Machine.MachineRef<State, Event, Error, Output>, StartError>>
): MachineAtom<State, Event, Error, Output, StartError> => {
const makeFromRefAtom = <State, Event, Error, Output, StartError, Emitted>(
ref: Atom.Atom<AsyncResult.AsyncResult<Machine.MachineRef<State, Event, Error, Output, Emitted>, StartError>>
): MachineAtom<State, Event, Error, Output, StartError, Emitted> => {
const snapshot = Atom.readable((
get
): AsyncResult.AsyncResult<Machine.RuntimeSnapshot<State, Error, Output>, StartError> => {
Expand Down Expand Up @@ -513,9 +513,10 @@ export const select = <
Error,
Output,
StartError,
Emitted,
const Path extends ValuedSnapshotIdentifier<State>
>(
self: MachineAtom<State, Event, Error, Output, StartError>,
self: MachineAtom<State, Event, Error, Output, StartError, Emitted>,
path: Path
): Atom.Atom<
AsyncResult.AsyncResult<Option.Option<SnapshotValueByIdentifier<State, Path>>, StartError | Error>
Expand All @@ -530,9 +531,10 @@ export const selectSnapshot = <
Error,
Output,
StartError,
Emitted,
const Path extends SnapshotIdentifier<State>
>(
self: MachineAtom<State, Event, Error, Output, StartError>,
self: MachineAtom<State, Event, Error, Output, StartError, Emitted>,
path: Path
): Atom.Atom<
AsyncResult.AsyncResult<Option.Option<SnapshotByIdentifier<State, Path>>, StartError | Error>
Expand Down Expand Up @@ -583,9 +585,10 @@ export const matches = <
Error,
Output,
StartError,
Emitted,
const Path extends SnapshotIdentifier<State>
>(
self: MachineAtom<State, Event, Error, Output, StartError>,
self: MachineAtom<State, Event, Error, Output, StartError, Emitted>,
path: Path
): Atom.Atom<AsyncResult.AsyncResult<boolean, StartError | Error>> =>
Atom.mapResult(self.result, (snapshot) => Option.isSome(Topology.getSnapshotByPath(snapshot, path))).pipe(
Expand Down Expand Up @@ -708,7 +711,7 @@ const makeWithRuntime = (
runtime: Atom.AtomRuntime<any, any>,
machine: Machine.Machine.Any,
args: ReadonlyArray<unknown>
): MachineAtom<any, any, any, any, any> => {
): MachineAtom<any, any, any, any, any, any> => {
const prepared = runtime.atom(() => internalMachine.prepare(machine as any, ...(args as [])))
const ref = runtime.atom((get) => startPreparedMachineAtomEffect(get, prepared as any))
const result = makeFromRefAtom(ref as any)
Expand All @@ -720,7 +723,7 @@ const resumeWithRuntime = (
runtime: Atom.AtomRuntime<any, any>,
machine: Machine.Machine.Any,
snapshot: Machine.Machine.Snapshot<any>
): MachineAtom<any, any, any, any, any> => {
): MachineAtom<any, any, any, any, any, any> => {
const ref = runtime.atom((get) => resumeMachineAtomEffect(get, machine, snapshot))
return makeFromRefAtom(ref as any)
}
Expand Down
19 changes: 11 additions & 8 deletions src/unstable/reactivity/AtomMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,11 @@ export interface MachineAtom<State, Event, Error = never, Output = never, StartE
) => ChildMachineAtom<Child, StartError>
}

type RefState<Ref> = Ref extends Machine.MachineRef<infer State, any, any, any> ? State : never
type RefState<Ref> = Ref extends Machine.MachineRef<infer State, any, any, any, any> ? State : never

type RefError<Ref> = Ref extends Machine.MachineRef<any, any, infer Error, any> ? Error : never
type RefError<Ref> = Ref extends Machine.MachineRef<any, any, infer Error, any, any> ? Error : never

type RefOutput<Ref> = Ref extends Machine.MachineRef<any, any, any, infer Output> ? Output : never
type RefOutput<Ref> = Ref extends Machine.MachineRef<any, any, any, infer Output, any> ? Output : never
type RefEmitted<Ref> = Ref extends Machine.MachineRef<any, any, any, any, infer Emitted> ? Emitted : never

/**
Expand Down Expand Up @@ -305,7 +305,7 @@ export interface ChildMachineAtom<Child extends Machine.ChildMachine.Any, StartE
) => ChildMachineAtom<Nested, StartError>
}

type BridgeStartError<Bridge> = Bridge extends MachineAtom<any, any, any, any, infer StartError> ? StartError
type BridgeStartError<Bridge> = Bridge extends MachineAtom<any, never, any, any, infer StartError, any> ? StartError
: Bridge extends ChildMachineAtom<any, infer StartError> ? StartError
: never

Expand All @@ -317,7 +317,7 @@ type BridgeStartError<Bridge> = Bridge extends MachineAtom<any, any, any, any, i
* @since 0.4.0
*/
export type ChildOf<
Parent extends MachineAtom<any, any, any, any, any> | ChildMachineAtom<any, any>,
Parent extends MachineAtom<any, never, any, any, any, any> | ChildMachineAtom<any, any>,
Child extends Machine.ChildMachine.Any
> = ChildMachineAtom<Child, BridgeStartError<Parent>>

Expand Down Expand Up @@ -389,8 +389,9 @@ export const select: <
Error,
Output,
StartError,
Emitted,
const Path extends ValuedSnapshotIdentifier<State>
>(self: MachineAtom<State, Event, Error, Output, StartError>, path: Path) => Atom.Atom<
>(self: MachineAtom<State, Event, Error, Output, StartError, Emitted>, path: Path) => Atom.Atom<
AsyncResult.AsyncResult<Option.Option<SnapshotValueByIdentifier<State, Path>>, StartError | Error>
> = internal.select

Expand All @@ -410,8 +411,9 @@ export const selectSnapshot: <
Error,
Output,
StartError,
Emitted,
const Path extends SnapshotIdentifier<State>
>(self: MachineAtom<State, Event, Error, Output, StartError>, path: Path) => Atom.Atom<
>(self: MachineAtom<State, Event, Error, Output, StartError, Emitted>, path: Path) => Atom.Atom<
AsyncResult.AsyncResult<Option.Option<SnapshotByIdentifier<State, Path>>, StartError | Error>
> = internal.selectSnapshot

Expand Down Expand Up @@ -503,9 +505,10 @@ export const matches: <
Error,
Output,
StartError,
Emitted,
const Path extends SnapshotIdentifier<State>
>(
self: MachineAtom<State, Event, Error, Output, StartError>,
self: MachineAtom<State, Event, Error, Output, StartError, Emitted>,
path: Path
) => Atom.Atom<AsyncResult.AsyncResult<boolean, StartError | Error>> = internal.matches

Expand Down
9 changes: 9 additions & 0 deletions test/unstable/reactivity/AtomMachine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ describe("AtomMachine", () => {
})
const registry = yield* makeRegistry
const bridge = AtomMachine.make(machine)
const selected = AtomMachine.select(bridge, "Idle")
const selectedSnapshot = AtomMachine.selectSnapshot(bridge, "Idle")
const matched = AtomMachine.matches(bridge, "Idle")
const observed = yield* AtomMachine.emissions(bridge).pipe(
Stream.take(1),
Stream.runCollect,
Expand All @@ -133,6 +136,12 @@ describe("AtomMachine", () => {
)

assert.deepStrictEqual(Array.from(yield* Fiber.join(observed)), [new ReadyEmission({})])
assert.deepStrictEqual(yield* AtomRegistry.getResult(registry, selected), Option.some(new Idle({})))
assert.deepStrictEqual(
yield* AtomRegistry.getResult(registry, selectedSnapshot),
Option.some({ path: "Idle", value: new Idle({}) })
)
assert.strictEqual(yield* AtomRegistry.getResult(registry, matched), true)
assert.strictEqual((yield* AtomRegistry.getResult(registry, bridge.snapshot)).status, "active")
})))

Expand Down
77 changes: 76 additions & 1 deletion typetest/unstable/reactivity/AtomMachine.tst.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Context, Effect, Layer, type Option, Schema } from "effect"
import { Context, Effect, Layer, type Option, Schema, Stream } from "effect"
import { AsyncResult, Atom } from "effect/unstable/reactivity"
import { describe, expect, it } from "tstyche"
import { Machine } from "../../../src/index.js"
Expand All @@ -10,6 +10,8 @@ class Tick extends Schema.TaggedClass<Tick>("Tick")("Tick", {}) {}

class InternalTick extends Schema.TaggedClass<InternalTick>("InternalTick")("InternalTick", {}) {}

class Published extends Schema.TaggedClass<Published>("Published")("Published", {}) {}

class Done extends Schema.TaggedClass<Done>("Done")("Done", {
value: Schema.String
}) {}
Expand Down Expand Up @@ -63,6 +65,7 @@ interface RuntimeFailure {
}

const States = Machine.states({ Idle })
const Emissions = Machine.emittedEvents(Published)

const NestedStates = Machine.states({
Dormant,
Expand Down Expand Up @@ -118,6 +121,19 @@ const makeMachine = () =>
Idle: {}
})

const makeEmittingMachine = () =>
Machine.make({
states: States.states,
events: Machine.events(Tick),
emittedEvents: Emissions,
initial: {
target: (to) => to.Idle(),
resolve: ({ target }) => target(new Idle({}))
}
}).handle({
Idle: {}
})

describe("AtomMachine", () => {
it("derives invoked child protocols from the child descriptor", () => {
const childMachine = makeMachine()
Expand All @@ -142,6 +158,65 @@ describe("AtomMachine", () => {
>()
})

it("preserves emitted protocols across root and child selectors", () => {
const machine = makeEmittingMachine()
const direct = AtomMachine.make(machine)
const bound = AtomMachine.bind(Atom.runtime(Layer.empty)).make(machine)
const directSelected = AtomMachine.select(direct, "Idle")
const directSnapshot = AtomMachine.selectSnapshot(direct, "Idle")
const directMatched = AtomMachine.matches(direct, "Idle")
const boundSelected = AtomMachine.select(bound, "Idle")
const boundSnapshot = AtomMachine.selectSnapshot(bound, "Idle")
const boundMatched = AtomMachine.matches(bound, "Idle")

expect<Atom.Success<typeof directSelected>>().type.toBe<Option.Option<Idle>>()
expect<Atom.Success<typeof directSnapshot>>().type.toBe<
Option.Option<Machine.Machine.SnapshotByIdentifier<typeof States.states, "Idle">>
>()
expect<Atom.Success<typeof directMatched>>().type.toBe<boolean>()
expect<Atom.Success<typeof boundSelected>>().type.toBe<Option.Option<Idle>>()
expect<Atom.Success<typeof boundSnapshot>>().type.toBe<
Option.Option<Machine.Machine.SnapshotByIdentifier<typeof States.states, "Idle">>
>()
expect<Atom.Success<typeof boundMatched>>().type.toBe<boolean>()
expect(AtomMachine.select).type.not.toBeCallableWith(direct, "Missing")
expect(AtomMachine.selectSnapshot).type.not.toBeCallableWith(direct, "Missing")
expect(AtomMachine.matches).type.not.toBeCallableWith(direct, "Missing")

const Child = Machine.child("emitting-child", machine)
const parentMachine = Machine.make({
states: States.states,
events: Machine.events(),
emittedEvents: Emissions,
initial: {
target: (to) => to.Idle(),
resolve: ({ target }) => target(new Idle({}))
}
}).handle({
Idle: {
invoke: Machine.invoke({ child: Child })
}
})
const parent = AtomMachine.make(parentMachine)
const child = parent.child(Child)
const derivedChild = null as unknown as AtomMachine.ChildOf<typeof parent, typeof Child>
const childSelected = AtomMachine.selectChild(child, "Idle")
const childSnapshot = AtomMachine.selectSnapshotChild(child, "Idle")
const childMatched = AtomMachine.matchesChild(child, "Idle")
const childEmissions = AtomMachine.childEmissions(child)

expect<typeof derivedChild>().type.toBe<typeof child>()
expect<Atom.Success<typeof childSelected>>().type.toBe<Option.Option<Idle>>()
expect<Atom.Success<typeof childSnapshot>>().type.toBe<
Option.Option<Machine.Machine.SnapshotByIdentifier<typeof States.states, "Idle">>
>()
expect<Atom.Success<typeof childMatched>>().type.toBe<boolean>()
expect<Stream.Success<typeof childEmissions>>().type.toBe<Published>()
expect(AtomMachine.selectChild).type.not.toBeCallableWith(child, "Missing")
expect(AtomMachine.selectSnapshotChild).type.not.toBeCallableWith(child, "Missing")
expect(AtomMachine.matchesChild).type.not.toBeCallableWith(child, "Missing")
})

it("derives fail-aware results, selectors, and child bridge types", () => {
const childMachine = makeMachine()
const Child = Machine.child("child", childMachine)
Expand Down