refactor!: simplify the tag/getter API and fix #217 - #227
Open
linkdata wants to merge 2 commits into
Open
Conversation
Investigating #224 and #217 showed that most of the surface they touch exists only to support machinery nothing uses. Remove tag.Context. It was threaded through every JawsGetTag signature and all of expand's recursion, but no production implementation read it. Its only consumer was tag.MustTagExpand calling ctx.MustLog — an inversion that let lib/tag log through a *jaws.Request without importing jaws, at the cost of an unused parameter on every implementer. TagGetter becomes JawsGetTag() any and TagExpand becomes TagExpand(tagValue any) ([]any, error). Remove InitHandler/JawsInit. This is the mechanism #224 reported: it ran after tag resolution, so a getter that changed its tags there desynchronized registration from routing. It had no production implementation; its only uses were test fixtures exploiting it to fail a render. Element.ApplyGetter therefore drops its error result and returns (tagValue, attrs). Move MustTagExpand to *jaws.Jaws, where MustLog lives. Request.Dirty, Request.Tag, Request.GetElements, Jaws.Dirty and Jaws.Broadcast all route through it; ui.Template stays on tag.TagExpand so it can return expansion errors normally. Fix #217: bind.HTMLGetterFunc and bind.StringGetterFunc retained the caller's variadic tags slice header, so reusing that slice retagged a live getter. They now shallow-clone the top-level slots, which is documented along with the boundary — nested containers and reference-backed tag values stay the caller's obligation. Close #224 by documenting the TagGetter contract in lib/tag/taggetter.go: JawsGetTag is the canonical public accessor; ApplyGetter calls it exactly once per invocation but there is no global call-count guarantee; and, except for a documented initialization phase returning nil, a TagGetter must be idempotent in tag identity, with previously returned containers still expanding to the key set they produced and treated as read-only. JsVar is the one in-tree initialization case. JaWS does not serialize JawsGetTag calls. Jaws.Dirty's doc comment is rewritten: it claimed TagGetters are called with a nil Request (there is no context now), that Request.Dirty avoids that (the two are equivalent), and that a non-hashable tag panics — a path TagExpand makes unreachable by rejecting unusable keys before setDirty sees them. It now documents what a caller observes: with a Logger the expansion error is logged and the partial result still applied, without one the call panics before anything is applied. Request.Dirty, Request.Tag, Request.GetElements and Jaws.Broadcast get the same treatment, and Request.TagExpanded is marked as the advanced API that neither expands nor validates. Coverage stays at 99.6%; Request.MustLog gains a direct test now that tag expansion no longer reaches it incidentally.
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.
Closes #217. Closes #224.
Investigating both issues showed that most of the surface they touch exists only to support machinery nothing uses. Compatibility breaks are accepted; there are no out-of-repository widget implementations or
TagGetterconsumers to preserve.Remove
tag.ContextIt was threaded through every
JawsGetTagsignature and all ofexpand's recursion, but no production implementation read it. Its only consumer wastag.MustTagExpandcallingctx.MustLog— an inversion that letlib/taglog through a*jaws.Requestwithout importingjaws, at the cost of an unused parameter on every implementer.TagGetterbecomesJawsGetTag() anyTagExpandbecomesTagExpand(tagValue any) ([]any, error)lib/tag/context.gois deleted; thelib/tagimport drops out of six files whose only use wastag.Context, so their[tag.X]doc links are now fully qualifiedRequest.Initial,Get,Set,Context,LogandMustLogall stay — they are public Request API in their own right, not justtag.Contextplumbing.Remove
InitHandler/JawsInitThis is the mechanism #224 reported: it ran after tag resolution, so a getter that changed its tags there desynchronized registration from routing. It had no production implementation in this repo; its only uses were test fixtures exploiting it to make a render fail.
Element.ApplyGettertherefore drops its error result:The two fixtures that abused it are replaced by the render-error fixtures already present in package
ui(testRenderErrorUI,failNthWrite), so the genuineNewUIcleanup andrenderInnerwrite-error coverage survives.InitialHTMLAttrHandleris unaffected.Move
MustTagExpandto*jaws.JawsIt existed only to pair
TagExpandwithContext.MustLog, so it belongs whereMustLoglives:Request.Dirty,Request.Tag,Request.GetElements,Jaws.DirtyandJaws.Broadcastall route through it.ui.Templatestays ontag.TagExpandso it can return expansion errors normally.lib/tag's internal tests cannot importjaws(tag → jaws → tag), so the logging/panic coverage moved to the root package andlib/binduses a small localmustExpandhelper rather than constructing a*Jawsto expand a value.Fix #217
bind.HTMLGetterFuncandbind.StringGetterFuncretained the caller's variadictagsslice header, so reusing that slice retagged a live getter. They now shallow-clone the top-level slots. The doc comments state the boundary rather than overclaiming it: nested containers and reference-backed tag values are not copied and stay the caller's obligation. Mutation regressions sit next to each constructor.Close #224 as a contract clarification
Element.ApplyGetterdeliberately returns a candidate that widgets retain and re-expand, so the fix is to say what a getter owes.lib/tag/taggetter.gois now the authoritative statement:JawsGetTagis the canonical public accessor and application code may call it directly; callers wanting flattened, validated keys useTagExpand.ApplyGetterinvokes it to obtain a tag candidate, then expands that candidate for registration; the expansion may invokeJawsGetTagagain when the candidate is itself aTagGetteror contains one. Standard getter-backed widgets invokeApplyGetteronce during initial render, but there is noJawsGetTagcall-count guarantee:TagExpand, dirtying, broadcasts and application code may make further calls.TagGettermust be idempotent in tag identity. After its first non-nil result, every call must return a value thatTagExpandexpands to the same set of keys; previously returned containers must continue expanding to the key set they produced and must be treated as read-only. Fresh containers and equivalent representations are fine. Non-idempotent implementations are unsupported.JawsGetTagcalls; a getter used concurrently must synchronize its state and safely publish returned containers.ui.JsVaris documented as the one in-tree initialization case: nil before its first render initializes the dirty tag, and that nil is not a dirty target.All fifteen production
JawsGetTagimplementations were audited against this. The twolib/bindgetter-funcs were the only violations, which is exactly #217.Doc comments corrected along the way
Jaws.Dirty's comment claimed three things that are now false: thatTagGetters are called with a nilRequest(there is no context), thatRequest.Dirtyavoids that (the two are equivalent, andRequest.Dirtyis not even request-scoped — it delegates to Jaws-wide dirtying), and that a non-hashable tag panics — a pathTagExpandmakes unreachable by rejecting unusable keys beforesetDirtysees them. It now documents what a caller actually observes: with aLoggerthe expansion error is logged and the partial result is still applied; without one the call panics before anything is applied.Request.Dirty,Request.Tag,Request.GetElementsandJaws.Broadcastget the same treatment, andRequest.TagExpandedis marked as the advanced API that adds keys without expanding or validating them.Request.TagExpandedalso checks deletion while holding the Request lock, making concurrent deletion and tag registration linearizable. A deterministic regression test covers the stale pre-lock check that could re-register a deleted Element..agents/skills/jaws/SKILL.mdandlib/ui/README.mdare updated to match.Verification
gofmt,go generate,go build -v,go vet,staticcheck,golangci-lintandgosecare all clean. Tests pass under-race, under-tags debug -race, and in the plain production build (which exercises the releaseTagStringpath), all withJAWS_REQUIRE_NODE=1.lib/bindcompiles and vets clean underGOARCH=386 CGO_ENABLED=0(this host cannot exec 32-bit binaries).FuzzParseParamsran 30s clean after its recipe table was edited.Coverage holds at the 99.6% baseline.
Request.MustLoggained a direct test: it was previously covered incidentally throughtag.MustTagExpand(rq, …), which no longer exists.