Repository navigation
WebApi: Navigator and Scheduler get a real interface object and prototype - #3275
Merged
Merged
Conversation
…type (sebastienros#3270) Both were interface *instances* built without either, so their members were own properties of the singleton — the last two [JsObject] hosts under Jint/WebApi/ that were neither an interface prototype object, an interface object, nor a WebIDL namespace. Three consequences closed at once. The WebIDL attribute exemption. sebastienros#3239 had to leave navigator.userAgent, scheduler.postTask and scheduler.yield with an ECMAScript built-in's attributes, because WebIDL's enumerability rule assumes the member sits on the interface prototype object, where the enumerability is invisible from the instance. Flipping the flags where they were would have made Object.keys(navigator) answer ["userAgent"], which no implementation does. Moving the members is what fixes it, so the three entries in WebIdlPropertyAttributeTests.Divergences are deleted and that list is now empty; EveryRecordedDivergenceIsReal is what would have caught a stale one. The containment hazard sebastienros#3266 identified for these two and sebastienros#3257 left open. Measured before the change, in the REPL: navigator.__proto__.poisoned = 42 ({}).poisoned 42 Object.prototype own property: true scheduler.__proto__.poisoned = 43 ({}).poisoned 43 Object.prototype own property: true and after: undefined and false for both, with the value still reachable through the singleton, which is what a patching library is after. The own-key lists. Reflect.ownKeys(navigator) was ["userAgent", Symbol(Symbol.toStringTag)]; it is [] now, as it is in Node 24 and a browser. Same for scheduler. Node 24 is the oracle for Navigator and agrees line for line — typeof Navigator "function", name/length Navigator/0, the prototype chain, the constructor property's attributes, userAgent an enumerable accessor with no setter on Navigator.prototype, and "TypeError: Illegal constructor" from new Navigator(). Two lines differ, both deliberately: Jint's Navigator.prototype carries only userAgent, everything else being absent rather than faked; and Object.prototype.toString.call(navigator) stays [object Navigator] as WebIDL's class-string rule and a browser require, where Node answers [object Object] because its Navigator is an ordinary class. Node has no scheduler at all, so for that one the oracle is the WICG draft's IDL plus Chrome. Constructibility, checked per spec rather than assumed. Navigator is [Exposed=Window] and declares no constructor operation (https://html.spec.whatwg.org/multipage/system-state.html#the-navigator-object); Scheduler is [Exposed=(Window, Worker)] and declares none either (https://wicg.github.io/scheduling-apis/#sec-scheduler), unlike TaskController and TaskPriorityChangeEvent beside it. Neither constructs. Both interface objects are exposed as globals, following sebastienros#3250's pattern exactly: a lazy, non-clobbering descriptor on the principal realm's global object, WebIDL's interface-object attributes, never in a ShadowRealm. Navigator's [Exposed=Window] is not a reason to withhold it — the instance carries the same declaration and is a Jint global anyway because WinterTC asks for globalThis.navigator.userAgent, and Node's global is not a Window either yet exposes both. Scheduler joins the three interface objects its own feature already installs. Cost, measured with GC.GetAllocatedBytesForCurrentThread over 2,000 constructions, bit-identical on every repeat: new Engine() 9,256 -> 9,272 B +16 new Engine(o => o.UseWebApis()) 24,280 -> 24,392 B +112 UseWebApis() + Execute("1+1") 26,464 -> 26,576 B +112 UseWebApis(Navigator) 11,176 -> 11,288 B +112 UseWebApis(Scheduler) 12,032 -> 12,144 B +112 UseWebApis() + read navigator.userAgent 27,440 -> 27,912 B +472 UseWebApis() + read scheduler.postTask 27,584 -> 28,056 B +472 The +16 B on a default engine is two reference fields on Intrinsics. The +112 B is the two extra lazy global descriptors and the storage they land in; nothing materializes until a script names one, which InterfaceObjectExposureTests.CostsNothingUntilItIsRead now pins for both names and NavigatorTests pins for the singleton's own descriptor. The +472 B is what a script that actually reads a member pays for the interface object and prototype it did not have before. WPT: 422/422 before, 422/422 after, corpus untouched. No row moved, and none could have moved silently — the exclusion table refuses an entry that has started passing, so a green run is the assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd
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 #3270.
NavigatorandSchedulerget the interface object and interface prototype object WebIDL gives them, the two singletons' members move onto those prototypes, and the three exemptions #3272 had to carve out disappear —WebIdlPropertyAttributeTests.Divergencesis now[].The shape, per interface
Both take the usual triple: interface object →
[JsObject(UseShape = true)] : Prototype→ a plainObjectInstancesingleton with no own properties at all.Navigator—NavigatorConstructor→NavigatorPrototype(constructor,@@toStringTag="Navigator",userAgentasConfigurable | Enumerable) →JsNavigator, which carries no state and exists to be the brand.Scheduler—SchedulerConstructor→SchedulerPrototype(constructor,@@toStringTag,postTask/yieldasConfigurableEnumerableWritable) →JsScheduler, which holds theSchedulerQueueandTimerQueue. That is the split WebIDL draws and the oneJsPerformancealready uses: the members are the interface's, the queues are the instance's, and each member brand-checks its receiver and then operates on it — so an extractedpostTaskbehaves exactly as a browser's does.Neither is constructible. HTML's
[Exposed=Window] interface Navigator { }and the draft's[Exposed=(Window,Worker)] interface Scheduler { … }declare no constructor operation, so per WebIDL §3.7.1 each is a function that throws —TypeError: Illegal constructor, which is Node 24's exact message.TaskControllerandTaskPriorityChangeEventin the same draft do declare one, which is why they stay constructible and this does not.Why they are globals despite
[Exposed=Window]. That question was settled when the singletons were installed:navigatorcarries the same[Exposed=Window]declaration and is a Jint global anyway, because WinterTC asks a non-browser runtime forglobalThis.navigator.userAgent. Having decided the instance belongs, withholding the interface object leavesnavigator instanceof Navigatorunwritable while the object it names sits one link up the chain — the half-truth #3250 exists to remove. Node 24's global is not aWindoweither and exposes both. ForSchedulerthe draft status was already priced in: the feature installs three interface objects today, and withholding the fourth would make this the one API whose singleton cannot answerscheduler instanceof Schedulerwhile its own controller and signal can. The interface object has to exist regardless —Scheduler.prototype.constructoris it — so a global only settles reachability by name, and Chrome, the only implementation, exposes it.Both installs follow #3250: lazy non-clobbering
LazyPropertyDescriptor<Engine>on the principal realm's global, interface-object attributes{w:true, e:false, c:true}, never in aShadowRealm, behind the singleton's own feature flag.Against Node 24, run live: 16 of 18 identical
typeof Navigator,name/length,Reflect.ownKeys(navigator)→[],Object.keys(navigator)→[], both prototype links,instanceof, no ownSymbol.hasInstance,constructor === Navigator,userAgentan accessor with get/no-set/enumerable:true/configurable:trueonNavigator.prototype,new Navigator()→TypeError: Illegal constructor,constructor{w:t,e:f,c:t},prototype{w:f,e:f,c:f}, containment.Two differ, both deliberate:
Object.keys(Navigator.prototype)["userAgent"]hardwareConcurrency, locks, language, languages, platformObject.prototype.toString.call(navigator)[object Navigator][object Object]Navigatoris an ordinary JS class with no@@toStringTag; WebIDL's class-string rule and a browser both say[object Navigator], and Jint's pre-existing answer is preserved by moving the tag to the prototypeNode has no
schedulerat all, so there the oracle is the WICG IDL plus Chrome's shape.The containment hazard #3266 identified is closed
Before, both singletons were
BuiltinShapeObjects whose[[Prototype]]wasObject.prototype, so patching "the scheduler's prototype" patchedObject.prototype. Same script, before → after:({}).poisonedByNavigator42undefined'poisonedByNavigator' in {}truefalse[].poisonedByScheduler43undefinedObject.prototypeownpoisonedBy*truefalsegetPrototypeOf(navigator) === Object.prototypetruefalsePinned by
PatchingNavigatorsPrototypeDoesNotReachObjectPrototype, the scheduler twin, andGivesEachEngineItsOwnNavigatorPrototype(per-realm).Cost: +16 B on a default engine
GC.GetAllocatedBytesForCurrentThreadover 2,000 constructions, bit-identical on every repeat:new Engine()Intrinsicsnew Engine(o => o.UseWebApis())UseWebApis(Navigator)UseWebApis()+ readnavigator.userAgentNothing materializes until a script names one:
CostsNothingUntilItIsReadcovers both new names (descriptor stillCustomJsValue,_value == null), andNavigatorTests.IsAnEnumerableDataPropertyOfTheGlobalwas strengthened to assert the same of the singleton's own descriptor. The +472 B is what a script that actually reads a member pays for a prototype it did not have before.Verified failing first
Tests written and run against unfixed code: 20 failed / 129 passed, across
NavigatorTests×5,SchedulerTests×4,InterfaceObjectExposureTests×9,WebIdlPropertyAttributeTests×2. Representative:After: 149 / 149.
Suites
Release solution build clean on all five TFMs (1 warning = the pre-existing
MSB3277inJint.Tests.CommonScripts/net472, identical to baseline).Jint.TestsJint.TestsverifyingJint.Tests.PublicInterfaceJint.Tests.PublicInterfaceverifyingJint.Tests.CommonScriptsJint.Tests.SourceGeneratorsJint.Tests.Test262WPT 422 / 422 both sides, no row moved — and "before" was measured rather than assumed, from a
git archiveexport of the base commit built in a scratch directory. No row could have moved silently: the exclusion table refuses an entry that has started passing and the_minimumTestsfloors refuse a file that registered fewer, so a green run on both sides is itself the assertion. The corpus census inVendor/README.mdis unchanged. The only vendored files namingNavigatorare the twoworkers/WorkerNavigator*suites, both still excluded underNeedsDeclinedWorkerGlobals.The WinterTC conformance tables in
README.mddid not move, checked rather than assumed: §5.1's interface list — transcribed verbatim inWinterTcMinimumCommonApiTests.CommonInterfaces, the pin behind the table — names neitherNavigatornorScheduler, and the only navigator row is §5.2'snavigator.userAgent, whose provider is unchanged. Both new names went into the separate per-flag table an embedder actually reads.Deliberately left undone
WorkerNavigator. A worker still gets its parent'snavigator; divergences 6 and 7 in Design: Web Workers via a host-supplied worker provider #3167's ledger and their two WPT exclusions stand.[Replaceable]simplification stays —navigatorandschedulerremain ordinary enumerable data properties of the global rather than the accessor pair, the same simplificationconsole,crypto,performanceandselfcarry. Node'snavigatordescriptor is an accessor; that is the one remaining shape difference.Scheduler.prototype's brand check is not inAPrototypeMemberBrandChecksItsReceiver, because a promise-returning operation rejects rather than throws. The pre-existingSchedulerTests.NeitherOperationEverThrowscovers that half, and the omission is documented on the test.🤖 Generated with Claude Code