feat(temporal): detect Go in-workflow query/signal/update handler declarations#78
Conversation
…larations Surface workflow.SetQueryHandler / GetSignalChannel / SetUpdateHandler[WithOptions] calls as via=temporal.handler EdgeCalls edges carrying temporal_kind (query/signal/update) + temporal_name, originating from the enclosing workflow function. This mirrors the Java side's per-method @QueryMethod / @SignalMethod / @UpdateMethod annotation edges, giving the graph a symmetric, queryable record of the named handlers each Go workflow exposes. High-precision: only the canonical "workflow" receiver alias is matched and the handler name must be a string literal (runtime-matched names can't be pinned from a variable), consistent with the existing dispatch detector. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nnelWithOptions Complete the in-workflow handler set: the detector already handled SetUpdateHandlerWithOptions but missed the WithOptions variants of the query and signal handlers. Note: the Go SDK has no SetSignalHandler — signals are received via GetSignalChannel[WithOptions]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
f650ec2 to
87c4baf
Compare
| // Temporal in-workflow handler declaration: | ||
| // `workflow.SetQueryHandler(ctx, "name", fn)` etc. | ||
| if name := goTemporalHandlerName(expr.Node, src); name != "" { | ||
| dc.tempHandlerKind = hkind |
There was a problem hiding this comment.
@avfirsov I need to think about it. My main concern for now: why not do the same as with the Register kind?
dc.tempKind = "register_" + kind`In such a case, it could be
dc.tempKind = "handle_" + kind`without exploding the schema.
I'll take a look at other PRs and return to this point later. There might be benefits for resolution purposes, so I'm not sure atm.
There was a problem hiding this comment.
Good question — and you're right that the goDeferredCall field is the part worth trimming. Let me separate the two axes, because I think they answer your "benefits for resolution?" doubt:
The via value is the real schema; the struct field is just a parser-internal carrier. The reason I didn't fold this into tempKind is that handler declarations sit on a different axis than dispatch/register:
temporal.stub(ExecuteActivity) andtemporal.registerare about the activity/workflow namespace, andtemporal.stubedges get rewritten byResolveTemporalCalls(placeholder → real function). ThetempKindvalues (activity/workflow/register_activity/register_workflow) all live on that one axis and feed that one resolver pass.temporal.handleris the query/signal/update namespace and is not rewritten — it's pure provider metadata (“this workflow serves querystatus”), the Go-side counterpart of Java's@QueryMethod/@SignalMethod/@UpdateMethodannotation edges. A distinctvialets a consumer ask "all handlers" (via == "temporal.handler") without decoding ahandle_*prefix out oftempKind, and keeps the dispatch-intercept condition (tempKind == "activity" || "workflow") untouched.
So I'd keep the distinct via=temporal.handler (different namespace, different resolver treatment), but I fully agree about not adding the field. I can drop tempHandlerKind and carry it as tempKind = "handle_" + kind, with applyGoTemporalHandlerMeta routing on that prefix — exactly mirroring register_. Same for the tempOutKind field in #81 (signalout_/querycall_). That removes the schema growth you're flagging while preserving the semantic/via distinction.
No rush — happy to push that refactor whenever you've settled on the direction (or leave it as-is if you decide the separate field reads cleaner). Thanks for taking the time across the three PRs.
Summary
Surface Go-side Temporal in-workflow handler declarations (query / signal / update) as graph edges, symmetric with the Java side's existing
@QueryMethod/@SignalMethod/@UpdateMethodannotation edges.This is one half of a split from #75 (the other half — env-default dispatch names — is in a separate PR so each can be reviewed independently).
Changes
workflow.SetQueryHandler/workflow.GetSignalChannel/workflow.SetUpdateHandler[WithOptions]and emits avia=temporal.handlerEdgeCallsedge from the enclosing workflow function, carryingtemporal_kind(query/signal/update) +temporal_name(the handler's string name).via=temporal.registeremission (applyGoTemporalRegisterMeta) exactly — same edge shape, same stamping site — so the graph gains a queryable, per-workflow record of the named handlers a Go workflow exposes.workflowreceiver alias matches, and the handler name must be a string literal (query/signal/update names are matched by string at runtime, so a non-literal can't be pinned).Testing
go test -race ./...) for the touched packages (internal/parser/languages,internal/indexer). The only failures in my environment are the pre-existinginternal/indexerwatcher tests failing withinotify ... too many open files(a local WSL fd-limit issue, unrelated — no watcher files touched).go_temporal_test.go: query/signal/update detection + negative cases for non-literal name and aliased import) and end-to-end (temporal_e2e_test.go).Checklist
applyGoTemporalRegisterMeta, the canonical-alias-only stance of the dispatch detector)Meta["methods"]for interfaces (if applicable) — n/aEdgeMemberOfedges to their containing type (if applicable) — n/aNotes
The natural consumer of this data is the cross-language Java→Go match (a Java
@QueryMethod(name="status")consumer ↔ this GoSetQueryHandler(ctx, "status", …)provider), which is proposed for design discussion in #77 — out of scope here; this PR only adds the Go-side provider edges.