Skip to content

Commit 447f24a

Browse files
Merge pull request #5 from typeonce-dev/codex/fix-bound-machine-inference
fix: preserve bound machine inference
2 parents 3024ce9 + 955663f commit 447f24a

8 files changed

Lines changed: 665 additions & 174 deletions

File tree

.changeset/steady-machines-bind.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@typeonce/effect-machine": patch
3+
---
4+
5+
Preserve every machine protocol channel when creating bound AtomMachine bridges from deeply composed handled machines. Inline invoked children also retain their exact error, service, event, and output types instead of inheriting erased contextual `any` channels.
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { Context, Effect, Layer, Schema } from "effect"
2+
import { Atom } from "effect/unstable/reactivity"
3+
import { Machine } from "@typeonce/effect-machine"
4+
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
5+
6+
type Equal<Left, Right> =
7+
(<Type>() => Type extends Left ? 1 : 2) extends <Type>() => Type extends Right ? 1 : 2 ? true : false
8+
type Expect<Type extends true> = Type
9+
10+
class ExternalService extends Context.Service<ExternalService, string>()("consumer/ExternalService") {}
11+
class InitialService extends Context.Service<InitialService, string>()("consumer/InitialService") {}
12+
13+
class InitialFailure {
14+
readonly _tag = "InitialFailure"
15+
}
16+
class TransitionFailure {
17+
readonly _tag = "TransitionFailure"
18+
}
19+
class ActionFailure {
20+
readonly _tag = "ActionFailure"
21+
}
22+
class RuntimeFailure {
23+
readonly _tag = "RuntimeFailure"
24+
}
25+
26+
const State = Schema.TaggedUnion({
27+
Idle: {},
28+
Ready: {},
29+
Editor: {},
30+
Editing: { value: Schema.String },
31+
Saving: { value: Schema.String },
32+
Done: { value: Schema.String }
33+
})
34+
const Event = Schema.TaggedUnion({
35+
Begin: {},
36+
Save: { value: Schema.String }
37+
})
38+
const Internal = Schema.TaggedUnion({
39+
Loaded: { value: Schema.String },
40+
ChildCompleted: { value: Schema.String },
41+
ChildNotice: { value: Schema.String }
42+
})
43+
const Emitted = Schema.TaggedUnion({
44+
Notice: { value: Schema.String }
45+
})
46+
const ChildState = Schema.TaggedUnion({
47+
Done: { value: Schema.String }
48+
})
49+
50+
const ChildStates = Machine.defineStates({
51+
Done: {
52+
schema: ChildState.cases.Done,
53+
type: "final",
54+
output: Schema.String
55+
}
56+
})
57+
const childMachine = Machine.make({
58+
states: ChildStates.states,
59+
events: [],
60+
emits: [Internal.cases.ChildNotice],
61+
input: Schema.Struct({ value: Schema.String }),
62+
initial: ({ value }) => ChildStates.initial.Done(ChildState.cases.Done.make({ value }))
63+
}).handle({
64+
Done: {
65+
entry: ({ state }) =>
66+
Machine.action(
67+
Effect.gen(function* () {
68+
const runtime = yield* Machine.runtime<{ readonly emits: typeof Internal.cases.ChildNotice.Type }>()
69+
yield* runtime.sendParent(Internal.cases.ChildNotice.make({ value: state.value }))
70+
})
71+
),
72+
output: ({ state }) => state.value
73+
}
74+
})
75+
const Child = Machine.child("child", childMachine)
76+
77+
const States = Machine.defineStates({
78+
Idle: State.cases.Idle,
79+
Ready: {
80+
schema: State.cases.Ready,
81+
initial: "Editor",
82+
states: {
83+
Editor: {
84+
schema: State.cases.Editor,
85+
initial: "Editing",
86+
states: {
87+
Editing: State.cases.Editing,
88+
Saving: State.cases.Saving
89+
}
90+
}
91+
}
92+
},
93+
Done: {
94+
schema: State.cases.Done,
95+
type: "final",
96+
output: Schema.String
97+
}
98+
})
99+
100+
const machine = Machine.make({
101+
states: States.states,
102+
events: [Event.cases.Begin, Event.cases.Save],
103+
internalEvents: [Internal.cases.Loaded, Internal.cases.ChildCompleted, ...childMachine.emits],
104+
emits: [Emitted.cases.Notice],
105+
input: Schema.Struct({ seed: Schema.String }),
106+
initial: ({ seed }) =>
107+
Effect.gen(function* () {
108+
yield* InitialService
109+
if (seed.length < 0) return yield* Effect.fail(new InitialFailure())
110+
return States.initial.Idle(State.cases.Idle.make({}))
111+
})
112+
}).handle({
113+
Idle: {
114+
invoke: Machine.invoke({
115+
id: "deep-inline-invoke",
116+
src: () => Machine.effect(Effect.as(ExternalService, Internal.cases.Loaded.make({ value: "loaded" })))
117+
}),
118+
on: {
119+
Begin: ({ target }) =>
120+
target.full.Ready(State.cases.Ready.make({}), (ready) =>
121+
ready.Editor(State.cases.Editor.make({}), (editor) =>
122+
editor.Editing(State.cases.Editing.make({ value: "ready" }))
123+
)
124+
)
125+
}
126+
},
127+
Ready: {
128+
states: {
129+
Editor: {
130+
states: {
131+
Editing: {
132+
on: {
133+
Save: ({ event, target }) =>
134+
Machine.action(
135+
Effect.gen(function* () {
136+
yield* ExternalService
137+
return yield* Effect.fail(new ActionFailure())
138+
}),
139+
target.local.Saving(State.cases.Saving.make({ value: event.value }))
140+
),
141+
Loaded: () => Effect.fail(new TransitionFailure())
142+
}
143+
},
144+
Saving: {
145+
invoke: ({ state }) =>
146+
Machine.invokeMachine({
147+
child: Child,
148+
input: { value: state.value },
149+
onDone: ({ output }) => Internal.cases.ChildCompleted.make({ value: output })
150+
}),
151+
on: {
152+
ChildNotice: ({ event, target }) =>
153+
Machine.action(
154+
Effect.gen(function* () {
155+
const runtime = yield* Machine.runtime<{ readonly emits: typeof Emitted.cases.Notice.Type }>()
156+
yield* runtime.sendParent(Emitted.cases.Notice.make({ value: event.value }))
157+
}),
158+
target.local.Saving(State.cases.Saving.make({ value: event.value }))
159+
),
160+
ChildCompleted: ({ event, target }) => target.full.Done(State.cases.Done.make({ value: event.value }))
161+
}
162+
}
163+
}
164+
}
165+
}
166+
},
167+
Done: {
168+
output: ({ state }) => state.value
169+
}
170+
})
171+
172+
const runtime = Atom.runtime(
173+
Layer.mergeAll(
174+
Layer.succeed(ExternalService, "provided"),
175+
Layer.succeed(InitialService, "provided"),
176+
Layer.effectDiscard(Effect.fail(new RuntimeFailure()))
177+
)
178+
)
179+
const Bound = AtomMachine.bind(runtime)
180+
const machineAtom = Bound.make(machine, { seed: "initial" })
181+
182+
type Snapshot = Machine.Machine.Snapshot<typeof States.states>
183+
type StateSuccess = Atom.Success<typeof machineAtom.state>
184+
type SendEvent = typeof machineAtom.send extends Atom.Writable<any, infer InputEvent> ? InputEvent : never
185+
type Output = typeof machineAtom extends AtomMachine.MachineAtom<any, any, any, infer Value, any> ? Value : never
186+
type Failure = Atom.Failure<typeof machineAtom.result>
187+
188+
type StateIsExact = Expect<Equal<StateSuccess, Snapshot>>
189+
type EventsArePublicOnly = Expect<Equal<SendEvent, typeof Event.Type>>
190+
type OutputIsExact = Expect<Equal<Output, string>>
191+
type InitialErrorIsPreserved = Expect<Equal<Extract<Failure, InitialFailure>, InitialFailure>>
192+
type TransitionErrorIsPreserved = Expect<Equal<Extract<Failure, TransitionFailure>, TransitionFailure>>
193+
type ActionErrorIsPreserved = Expect<Equal<Extract<Failure, ActionFailure>, ActionFailure>>
194+
type RuntimeErrorIsPreserved = Expect<Equal<Extract<Failure, RuntimeFailure>, RuntimeFailure>>
195+
type FailureIsNotUnknown = Expect<Equal<unknown extends Failure ? true : false, false>>
196+
type MachineServicesAreNotAny = Expect<
197+
Equal<0 extends 1 & Machine.Machine.Services<typeof machine> ? true : false, false>
198+
>
199+
200+
// @ts-expect-error Input is required.
201+
Bound.make(machine)
202+
// @ts-expect-error Input retains its exact decoded type.
203+
Bound.make(machine, { seed: 1 })
204+
// @ts-expect-error The bound runtime must provide every external service.
205+
AtomMachine.bind(Atom.runtime(Layer.empty)).make(machine, { seed: "initial" })
206+
const erased: Machine.Machine.Any = machine
207+
// @ts-expect-error Machine.Any erasure cannot manufacture concrete protocol or output proof.
208+
Bound.make(erased, { seed: "initial" })
209+
210+
void machineAtom
211+
export type {
212+
ActionErrorIsPreserved,
213+
EventsArePublicOnly,
214+
FailureIsNotUnknown,
215+
InitialErrorIsPreserved,
216+
MachineServicesAreNotAny,
217+
OutputIsExact,
218+
RuntimeErrorIsPreserved,
219+
StateIsExact,
220+
TransitionErrorIsPreserved
221+
}

scripts/fixtures/consumer/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@
1111
"exactOptionalPropertyTypes": true,
1212
"types": ["node"]
1313
},
14-
"include": ["consumer.ts", "effect-beta-compat.d.ts"]
14+
"include": ["consumer.ts", "deep-bound.ts", "effect-beta-compat.d.ts"]
1515
}

scripts/test-consumer.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ try {
4646
[join(root, "node_modules", "typescript", "bin", "tsc"), "-p", join(consumer, "tsconfig.json")],
4747
{ cwd: consumer }
4848
)
49+
if (process.env.TSGO_BIN !== undefined) {
50+
run(process.env.TSGO_BIN, ["-p", join(consumer, "tsconfig.json")], { cwd: consumer })
51+
}
4952
run(process.execPath, [join(consumer, "runtime.mjs")], { cwd: consumer })
5053

5154
console.log("packed root, reactivity, and cluster entrypoints passed strict consumer validation")

src/AtomMachine.ts

Lines changed: 27 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -761,27 +761,12 @@ export const matchesChild = <
761761

762762
const BoundRequirementsTypeId = "~effect/reactivity/AtomMachine/BoundRequirements"
763763

764-
type MachineRequirementsOf<M extends Machine.Machine.Any> = M extends Machine.Machine<
765-
any,
766-
infer Events,
767-
any,
768-
any,
769-
any,
770-
infer R,
771-
any,
772-
infer InitialR,
773-
any,
774-
any,
775-
infer Emits,
776-
any,
777-
any
778-
> ? MachineRequirements<
779-
InitialR,
780-
R,
781-
Machine.Machine.EventOf<Events>,
782-
Machine.Machine.EmitOf<Emits>
783-
>
784-
: never
764+
type MachineRequirementsOf<M extends Machine.Machine.Any> = MachineRequirements<
765+
Machine.Machine.InitialServices<M>,
766+
Machine.Machine.Services<M>,
767+
Machine.Machine.Event<M>,
768+
Machine.Machine.Emit<M>
769+
>
785770

786771
type MissingBoundRequirements<Services, M extends Machine.Machine.Any> = Exclude<
787772
ExternalRequirements<MachineRequirementsOf<M>>,
@@ -797,65 +782,29 @@ type EnsureBoundRequirements<Services, M extends Machine.Machine.Any> =
797782
readonly [BoundRequirementsTypeId]: MissingBoundRequirements<Services, M>
798783
}
799784

800-
type EnsureMachineOutputImplementations<M extends Machine.Machine.Any> = M extends Machine.Machine<
801-
infer States,
802-
any,
803-
any,
804-
any,
805-
any,
806-
any,
807-
any,
808-
any,
809-
any,
810-
any,
811-
any,
812-
infer OutputStates,
813-
any
814-
> ? IsAny<States> extends true ? {
785+
type EnsureMachineOutputImplementations<M extends Machine.Machine.Any> =
786+
IsAny<Machine.Machine.States<M>> extends true ? {
815787
readonly "~effect/reactivity/AtomMachine/ConcreteMachineRequired": M
816788
}
817-
: Machine.Machine.EnsureOutputImplementations<States, OutputStates>
818-
: never
789+
: Machine.Machine.EnsureOutputImplementations<Machine.Machine.States<M>, Machine.Machine.OutputStates<M>>
819790

820-
type MachineInputArgsOf<M extends Machine.Machine.Any> = M extends Machine.Machine<
821-
any,
822-
any,
823-
infer Input,
824-
any,
825-
any,
826-
any,
827-
any,
828-
any,
829-
any,
830-
any,
831-
any,
832-
any,
833-
any
834-
> ? [...Machine.Machine.InputArgs<Input>]
835-
: never
791+
type MachineInputArgsOf<M extends Machine.Machine.Any> = [
792+
...Machine.Machine.InputArgs<Machine.Machine.Input<M>>
793+
]
836794

837-
type MachineAtomOf<M extends Machine.Machine.Any, RuntimeError> = M extends Machine.Machine<
838-
infer States,
839-
any,
840-
any,
841-
any,
842-
infer E,
843-
infer R,
844-
infer InitialE,
845-
infer InitialR,
846-
any,
847-
infer Output,
848-
any,
849-
any,
850-
any
851-
> ? MachineAtom<
852-
Machine.Machine.Snapshot<States>,
795+
type MachineAtomOf<M extends Machine.Machine.Any, RuntimeError> = MachineAtom<
796+
Machine.Machine.Snapshot<Machine.Machine.States<M>>,
853797
Machine.Machine.InputEvent<M>,
854-
MachineRuntimeError<E, R>,
855-
Output,
856-
MachineStartError<InitialE, E, InitialR, R, RuntimeError>
798+
MachineRuntimeError<Machine.Machine.Error<M>, Machine.Machine.Services<M>>,
799+
Machine.Machine.Output<M>,
800+
MachineStartError<
801+
Machine.Machine.InitialError<M>,
802+
Machine.Machine.Error<M>,
803+
Machine.Machine.InitialServices<M>,
804+
Machine.Machine.Services<M>,
805+
RuntimeError
806+
>
857807
>
858-
: never
859808

860809
/**
861810
* An `AtomMachine` factory with one owned Effect runtime.
@@ -873,7 +822,10 @@ export interface Bound<Services, RuntimeError = never> {
873822
* @since 4.0.0
874823
*/
875824
readonly make: <M extends Machine.Machine.Any>(
876-
machine: M & EnsureBoundRequirements<Services, M> & EnsureMachineOutputImplementations<M>,
825+
machine:
826+
& M
827+
& EnsureBoundRequirements<Services, NoInfer<M>>
828+
& EnsureMachineOutputImplementations<NoInfer<M>>,
877829
...args: MachineInputArgsOf<M>
878830
) => MachineAtomOf<M, RuntimeError>
879831
}

0 commit comments

Comments
 (0)