Wave 3 (0.5.0): JSClockSource seam and deterministic scheduler tests - #20
Merged
Conversation
JSScheduler held an undocumented hard dependency on JSDate.now() and JSTimer, making every timing behavior untestable without a live JS runtime and leaving the single-thread invariant invisible to the type system. All time observation and timer creation now flow through a single injectable seam (JSClockSource + JSClockCancellable token), with DefaultJSClockSource reproducing the historical behavior bit for bit. The additive init(clock:) is a separate initializer rather than a defaulted parameter so the existing init() keeps its mangled signature (API digester stays green). The class doc now records the chosen isolation model: non-Sendable by contract — @unchecked Sendable was rejected because Apple-side Combine may call a Sendable scheduler from arbitrary threads, where the JS event-loop invariant does not hold.
Timing-sensitive scheduler behavior was previously only testable on wasm with real timers and jitter tolerance bands. VirtualClock is a manually advanced JSClockSource (due-time order, creation-order ties, JS clamping semantics) needing no JS runtime, so the new exact-value variants of the SC-xx timing tests (VC-01..VC-11) run on the host lane too — host coverage grows 24 to 35 tests. Two wasm-gated tests (VC-A1/VC-A2) prove the async sleep/timer bridges are driven by the injected clock end to end. The real-timer wasm suites are kept unchanged as integration coverage.
The seam only pays off if consumers can discover it: the module overview now explains how to inject a manually advanced clock for deterministic, runtime-independent scheduler tests, the components table and a new Clock Injection topic group surface JSClockSource / JSClockCancellable / DefaultJSClockSource / init(clock:), and the CHANGELOG opens the Unreleased (0.5.0) section recording that the #14 surface is strictly additive and why @unchecked Sendable was rejected.
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.
Motivation
Wave 3 first slice (issue #14, target 0.5.0). JSScheduler's timing logic was only testable against real JS timers with tolerance bands; its environment coupling (
JSDate/JSTimer) was hard-wired. This PR introduces the injectable clock seam designed in the modernization analysis — strictly additive — and makes scheduler logic deterministically testable on the host lane.Changes
JSClockSourceprotocol +DefaultJSClockSource(newJSClockSource.swift):now+makeTimer(millisecondsDelay:isRepeating:callback:)mirroring JSTimer's shape; the default reproduces today'sJSDate.now()/JSTimerbehavior exactly. All scheduling paths (immediate, one-shot, repeating — and transitivelysleep(for:)/timer(interval:)) route through the seam.init(clock:)— a separate initializer, not a defaulted parameter (a default would removeinit()'s mangled symbol; verified with the digester).VirtualClocktest double + 11 deterministic test variants (exact-value, host-runnable) of the timing suite, plus 2 wasm-gated tests proving the async APIs are driven by the injected clock. Existing wasm real-timer tests stay as integration coverage.@unchecked Sendableevaluated and rejected — since Dual-backend support: #if canImport(Combine) shim for Apple platforms + WASM OpenCombine #11, Apple builds use native Combine, which may legally call aSendablescheduler from arbitrary threads, so the single-thread JS invariant cannot back the guarantee there. Strict-concurrency (-strict-concurrency=complete): 0 diagnostics before and after.Unreleased (0.5.0).Test plan
js testunder Node, re-run stable).diagnose-api-breaking-changes→ No breaking changes detected (gate should be green).swiftformat --lintand DocC build clean.Closes #14