Repository navigation
Type planner target resolution and runtime child operations - #261
Merged
Merged
Conversation
Contributor
Type performanceMeasured with TypeScript 6.0.3 and
Marginal instantiations are measured against the matching setup without that API call:
Check times (informational)
Type instantiations are the comparison metric. Check time varies with runner load and is informational only. |
Contributor
Runtime performanceMedian of 5 independent benchmark processes on Pull request baseline
Process runtime reference points
Effect Machine change from base
Process runtime reference change from base
Regression guardNo large, noise-adjusted throughput or heap regressions detected. Versions and interpretation
Higher throughput is better; lower heap is better. Variability is the median absolute deviation across independent processes, relative to their median. Small differences on shared GitHub-hosted hardware remain informational; the required guard rejects only large changes beyond the measured noise allowance. |
Build resolved history and initial targets through one erased constructor, type choice routes, let validating normalizers accept unknown input, and expose the transition event through the evaluated context's type. Replace the overloaded internal spawn, sendTo, and stopChild with erased implementations checked by the public Machine signatures, and type invocation sources as event-agnostic process logic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SandroMaglione
force-pushed
the
planner-typed-transitions
branch
from
November 1, 2026 09:38
79c96d4 to
ac855f3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is part 2 of the planner/execution-layer cast cleanup; part 1 (#258) covers
executionPlan.ts. The two PRs are independent: each branches frommain, they touch disjoint hunks, and I checked locally that they merge cleanly and that the combined tree type-checks and passes all 901 tests. No public API change. All emitted.d.tschanges are underinternal/, which the package does not export.Casts (
grep -c "as any"/grep -c "as unknown as"):internal/machine/planner.tsinternal/machine/machine.tsinternal/machine/invocation.tsinternal/testing/machine/finiteModel.tspackages/*/srctotalHow:
resolveInitialTargetandresolveHistoryTargetrebuilt targets with the typed publicmakeTarget(path as any, value as any, { snapshot: snapshot as any, values: values as any }). They now useTopology.makeResolvedTarget, an erased constructor for already-decoded targets. It has one documented assertion, needed because it keepsmakeTarget's exact own-property shape (valuespresent when undefined), which the optional publicvaluesfield cannot express.resolveChoiceTargetnow returnsChoiceRoute(Target | Snapshot | HistoryTarget | InitialTarget), narrowed by its existing final check, instead ofunknown. Additional choice targets areResolvedTarget.getTargetNodePathandnormalizeTargetConfigurationSyncalready check their input at runtime and throw for anything else, so they now acceptunknowninstead of fake-genericStatesunions.normalizeConfigurationSynctakesAtomicSnapshot<string, unknown>, which is what it forwards. Three now-redundantas Machine.Snapshot<any> | Machine.Target<any, any>assertions go away with them. Two white-box tests drop the type argument they passed.collectEvaluatedTransitionconstrainsContext extends { readonly event?: unknown }, replacing five(selection.context as any).event.as unknown as MicrostepTransition<…>and themachine as any,decodedEvent as any, andoutput as anysites incanSync,settle, andmacrostepConfigurationcompile without any cast.machine.ts). Internalspawn,sendTo, andstopChildduplicated the public overload signatures and were assigned withas any. Following the pattern from Check public Machine operations against their implementations #223, they are now erased implementations, andMachine.tschecks them structurally against its public overloads. The duplicateSpawnResult/SpawnErrorhelper types were removed.invocation.ts).oneShotandstreamLogicare typed asProcessLogicthat ignores events (Event = unknown), so they fitAnyConfig.srcwithoutas unknown as. Timer durations narrowraw.afterdirectly.ChildOwner.spawnforwards through an explicitly generic wrapper (scope.spawn<Child>(child, ...options)) instead of(scope.spawn as any).Casts deliberately kept, each with a one-line comment:
planner.ts:completeConfigurationSync(…, decodedEvent as any). Completion contexts receive the triggering event, which can be a private invocation event that the publicLifecycleEventtype does not model. Widening it would mean importing invocation events intoconfiguration.ts.machine.ts:stateNodeskeepsas unknown as. Compiled nodes are stored with string paths, and only the generic machine type carries their literal identifiers. (getSnapshotdropped itsunknownhop.)finiteModel.ts: three casts remain where a runtime-built model enters the literal-typed public constructors. The redundant innereventsFromSchemas(...) as anywas removed.Behavior. I compiled
srcfor base and PR and diffed the output. Apart from comments and shorthand properties, there are three changes, all equivalent for every call the public types allow:Machine.spawn(logic, options)now forwards{ id: options.id }and callsruntime.spawn(logic)when no id is given. The runtime already treated an absent id and no options identically.SpawnOptionsdeclares onlyid, so internal spawn fields (onOutcome,descriptor) that an untyped caller could previously smuggle through are no longer forwarded.ChildOwner.spawnspreads its optional options argument instead of always passing a second argument. The runtime readsoptions?.inputeither way.typeof after === "function"check thatresolveValueperformed.Changeset
.changeset/typed-planner-transitions.md, patch for@typeonce/effect-machinewith an "Internal: …" entry, following the same policy as Type the compiled execution plan without any casts #258 and Check public Machine operations against their implementations #223.Validation
pnpm check(92 files / 901 tests, all tstyche suites, pack and consumer checks), including the forced generic-versus-compiled differential suitespnpm perf:typesshowed 0 instantiation change in every scenario. Type-checkingpackages/effect-machineitself went from 419,337 to 413,361 instantiations (−5,976, −1.4%) and from 91,980 to 91,544 types. Check time showed no measurable difference in interleaved runs (≈2.5 s for both).pnpm perf:runtimebase and PR twice each, alternating. Rows with low variability moved by ±3% (plan counter +3.2%, compound −3.1%, parallel −2.0%), and memory was unchanged. Rows with 34–71% MAD swung both ways because of host load. The CI base-versus-PR comparison is authoritative.🤖 Generated with Claude Code