From 83d886f15dfd969eff5e174e3b9708a5524108f2 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 15 Mar 2026 15:14:39 +0000 Subject: [PATCH 01/14] Bump Swift toolchain snapshots in `test.yml` Bumping to `2026-03-05` for 6.3 and `2026-03-09` for `main`. --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 69f63a27c..c63d53fc9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,17 +21,17 @@ jobs: JAVASCRIPTKIT_DISABLE_TRACING_TRAIT=1 - os: ubuntu-24.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-12-01-a/swift-DEVELOPMENT-SNAPSHOT-2025-12-01-a-ubuntu24.04.tar.gz + download-url: https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2026-03-09-a/swift-DEVELOPMENT-SNAPSHOT-2026-03-09-a-ubuntu24.04.tar.gz wasi-backend: Node target: "wasm32-unknown-wasip1" - os: ubuntu-24.04 toolchain: - download-url: https://download.swift.org/swift-6.3-branch/ubuntu2404/swift-6.3-DEVELOPMENT-SNAPSHOT-2025-12-05-a/swift-6.3-DEVELOPMENT-SNAPSHOT-2025-12-05-a-ubuntu24.04.tar.gz + download-url: https://download.swift.org/swift-6.3-branch/ubuntu2404/swift-6.3-DEVELOPMENT-SNAPSHOT-2026-03-05-a/swift-6.3-DEVELOPMENT-SNAPSHOT-2026-03-05-a-ubuntu24.04.tar.gz wasi-backend: Node target: "wasm32-unknown-wasip1" - os: ubuntu-22.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-12-01-a/swift-DEVELOPMENT-SNAPSHOT-2025-12-01-a-ubuntu22.04.tar.gz + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2026-03-09-a/swift-DEVELOPMENT-SNAPSHOT-2026-03-09-a-ubuntu22.04.tar.gz wasi-backend: Node target: "wasm32-unknown-wasip1-threads" From 7ce78d283e26f8c1eb15df4d298c58a835e7c9b8 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 16 Mar 2026 21:29:38 +0000 Subject: [PATCH 02/14] Fix concurrency `@_spi` --- .../JavaScriptEventLoop+ExecutorFactory.swift | 8 +++++++- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 7 +++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift index 7de4cb74a..aac444a36 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift @@ -5,7 +5,7 @@ // See: https://forums.swift.org/t/pitch-2-custom-main-and-global-executors/78437 #if compiler(>=6.3) -@_spi(ExperimentalCustomExecutors) import _Concurrency +@_spi(ExperimentalCustomExecutors) @_spi(ExperimentalScheduling) import _Concurrency #else import _Concurrency #endif @@ -42,6 +42,7 @@ extension JavaScriptEventLoop: SchedulingExecutor { ) { let duration: Duration // Handle clocks we know + #if !hasFeature(Embedded) if let _ = clock as? ContinuousClock { duration = delay as! ContinuousClock.Duration } else if let _ = clock as? SuspendingClock { @@ -56,6 +57,9 @@ extension JavaScriptEventLoop: SchedulingExecutor { ) return } + #else + fatalError("SchedulingExecutor.enqueue is not supported in embedded mode") + #endif let milliseconds = Self.delayInMilliseconds(from: duration) self.enqueue( UnownedJob(job), @@ -81,6 +85,7 @@ extension JavaScriptEventLoop: ExecutorFactory { JavaScriptEventLoop.shared.enqueue(job) } + #if !hasFeature(Embedded) func enqueue( _ job: consuming ExecutorJob, after delay: C.Duration, @@ -94,6 +99,7 @@ extension JavaScriptEventLoop: ExecutorFactory { clock: clock ) } + #endif func run() throws { try JavaScriptEventLoop.shared.run() } diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index aebc90d65..aec1441a5 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -123,13 +123,16 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { private static func installGlobalExecutorIsolated() { guard !didInstallGlobalExecutor else { return } didInstallGlobalExecutor = true - #if compiler(>=6.3) + #if compiler(>=6.3) && !hasFeature(Embedded) if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) { // For Swift 6.3 and above, we can use the new `ExecutorFactory` API _Concurrency._createExecutors(factory: JavaScriptEventLoop.self) } #else - // For Swift 6.1 and below, we need to install the global executor by hook API + // For Swift 6.1 and below, or Embedded Swift, we need to install + // the global executor by hook API. The ExecutorFactory mechanism + // does not work in Embedded Swift because ExecutorImpl.swift is + // excluded from the embedded Concurrency library. installByLegacyHook() #endif } From 9714319afc3ac86e221ee617bada41c5a187227b Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 16 Mar 2026 22:10:16 +0000 Subject: [PATCH 03/14] Downgrade `main` snapshot to old version --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c63d53fc9..22e5c2665 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: JAVASCRIPTKIT_DISABLE_TRACING_TRAIT=1 - os: ubuntu-24.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2026-03-09-a/swift-DEVELOPMENT-SNAPSHOT-2026-03-09-a-ubuntu24.04.tar.gz + download-url: https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-12-01-a/swift-DEVELOPMENT-SNAPSHOT-2025-12-01-a-ubuntu24.04.tar.gz wasi-backend: Node target: "wasm32-unknown-wasip1" - os: ubuntu-24.04 @@ -31,7 +31,7 @@ jobs: target: "wasm32-unknown-wasip1" - os: ubuntu-22.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2026-03-09-a/swift-DEVELOPMENT-SNAPSHOT-2026-03-09-a-ubuntu22.04.tar.gz + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-12-01-a/swift-DEVELOPMENT-SNAPSHOT-2025-12-01-a-ubuntu22.04.tar.gz wasi-backend: Node target: "wasm32-unknown-wasip1-threads" From 9230c86bcaea995e31d648c470d81d2016fe0ca5 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 16 Mar 2026 22:24:25 +0000 Subject: [PATCH 04/14] Fix 6.3 build error --- .../JavaScriptEventLoop+ExecutorFactory.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift index aac444a36..2b16e5110 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift @@ -48,7 +48,9 @@ extension JavaScriptEventLoop: SchedulingExecutor { } else if let _ = clock as? SuspendingClock { duration = delay as! SuspendingClock.Duration } else { - // Hand-off the scheduling work to Clock implementation for unknown clocks + #if compiler(>=6.4) + // Hand-off the scheduling work to Clock implementation for unknown clocks. + // Clock.enqueue is only available in the development branch (6.4+). clock.enqueue( job, on: self, @@ -56,6 +58,9 @@ extension JavaScriptEventLoop: SchedulingExecutor { tolerance: tolerance ) return + #else + fatalError("Unsupported clock type; only ContinuousClock and SuspendingClock are supported") + #endif } #else fatalError("SchedulingExecutor.enqueue is not supported in embedded mode") From 4c72b42c306fa40aaf5bb208daf237d26ca7e808 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 16 Mar 2026 23:23:59 +0000 Subject: [PATCH 05/14] EmbeddedApp/main.swift: Fix capitalization in print statement --- Examples/Embedded/Sources/EmbeddedApp/main.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Embedded/Sources/EmbeddedApp/main.swift b/Examples/Embedded/Sources/EmbeddedApp/main.swift index f6bf5b6ac..5e7f01a3c 100644 --- a/Examples/Embedded/Sources/EmbeddedApp/main.swift +++ b/Examples/Embedded/Sources/EmbeddedApp/main.swift @@ -3,7 +3,7 @@ import JavaScriptKit let alert = JSObject.global.alert.object! let document = JSObject.global.document -print("Hello from WASM, document title: \(document.title.string ?? "")") +print("Hello from Wasm, document title: \(document.title.string ?? "")") var count = 0 From 8a7ae30bd997a02b8569197ca61a8a8f8ede44e7 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 16 Mar 2026 23:24:14 +0000 Subject: [PATCH 06/14] Add `Examples/EmbeddedConcurrency` --- Examples/EmbeddedConcurrency/Package.swift | 23 +++ .../Sources/EmbeddedConcurrencyApp/App.swift | 131 ++++++++++++++++++ Examples/EmbeddedConcurrency/build.sh | 7 + Examples/EmbeddedConcurrency/run.mjs | 47 +++++++ 4 files changed, 208 insertions(+) create mode 100644 Examples/EmbeddedConcurrency/Package.swift create mode 100644 Examples/EmbeddedConcurrency/Sources/EmbeddedConcurrencyApp/App.swift create mode 100755 Examples/EmbeddedConcurrency/build.sh create mode 100644 Examples/EmbeddedConcurrency/run.mjs diff --git a/Examples/EmbeddedConcurrency/Package.swift b/Examples/EmbeddedConcurrency/Package.swift new file mode 100644 index 000000000..59dfecaca --- /dev/null +++ b/Examples/EmbeddedConcurrency/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "EmbeddedConcurrency", + dependencies: [ + .package(name: "JavaScriptKit", path: "../../") + ], + targets: [ + .executableTarget( + name: "EmbeddedConcurrencyApp", + dependencies: [ + "JavaScriptKit", + .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), + ], + swiftSettings: [ + .enableExperimentalFeature("Extern"), + .swiftLanguageMode(.v5), + ] + ) + ] +) diff --git a/Examples/EmbeddedConcurrency/Sources/EmbeddedConcurrencyApp/App.swift b/Examples/EmbeddedConcurrency/Sources/EmbeddedConcurrencyApp/App.swift new file mode 100644 index 000000000..07cb9de9e --- /dev/null +++ b/Examples/EmbeddedConcurrency/Sources/EmbeddedConcurrencyApp/App.swift @@ -0,0 +1,131 @@ +@preconcurrency import JavaScriptKit +@preconcurrency import JavaScriptEventLoop +import _Concurrency + +#if compiler(>=6.3) +typealias DefaultExecutorFactory = JavaScriptEventLoop +#endif + +nonisolated(unsafe) var testsPassed = 0 +nonisolated(unsafe) var testsFailed = 0 + +@MainActor +func check(_ condition: Bool, _ message: String) { + let console = JSObject.global.console + if condition { + testsPassed += 1 + _ = console.log("PASS: \(message)") + } else { + testsFailed += 1 + _ = console.log("FAIL: \(message)") + } +} + +@main +struct App { + static func main() async throws(JSException) { + JavaScriptEventLoop.installGlobalExecutor() + + // Test 1: Basic async/await with checked continuation + let value: Int = await withCheckedContinuation { cont in + cont.resume(returning: 42) + } + check(value == 42, "withCheckedContinuation returns correct value") + + // Test 2: Unsafe continuation + let value2: Int = await withUnsafeContinuation { cont in + cont.resume(returning: 7) + } + check(value2 == 7, "withUnsafeContinuation returns correct value") + + // Test 3: JSPromise creation and .value await + let promise = JSPromise(resolver: { resolve in + resolve(.success(JSValue.number(123))) + }) + let result: JSPromise.Result = await withUnsafeContinuation { continuation in + promise.then( + success: { + continuation.resume(returning: .success($0)) + return JSValue.undefined + }, + failure: { + continuation.resume(returning: .failure($0)) + return JSValue.undefined + } + ) + } + if case .success(let val) = result { + check(val.number == 123, "JSPromise.value resolves correctly") + } else { + check(false, "JSPromise.value resolves correctly") + } + + // Test 4: setTimeout-based delay via JSPromise + let startTime = JSObject.global.Date.now().number! + let delayValue: Int = await withUnsafeContinuation { cont in + _ = JSObject.global.setTimeout!( + JSOneshotClosure { _ in + cont.resume(returning: 42) + return .undefined + }, + 100 + ) + } + let elapsed = JSObject.global.Date.now().number! - startTime + check(delayValue == 42 && elapsed >= 90, "setTimeout delay works (\(elapsed)ms elapsed)") + + // Test 5: Multiple concurrent tasks (using withUnsafeContinuation to avoid nonisolated hop) + var results: [Int] = [] + let task1 = Task { return 1 } + let task2 = Task { return 2 } + let task3 = Task { return 3 } + let r1: Int = await withUnsafeContinuation { cont in + Task { cont.resume(returning: await task1.value) } + } + let r2: Int = await withUnsafeContinuation { cont in + Task { cont.resume(returning: await task2.value) } + } + let r3: Int = await withUnsafeContinuation { cont in + Task { cont.resume(returning: await task3.value) } + } + results.append(r1) + results.append(r2) + results.append(r3) + results.sort() + check(results == [1, 2, 3], "Concurrent tasks all complete") + + // Test 6: Promise chaining with .then + let chained = JSPromise(resolver: { resolve in + resolve(.success(JSValue.number(10))) + }).then(success: { value in + return JSValue.number(value.number! * 2) + }).then(success: { value in + return JSValue.number(value.number! + 5) + }) + let chainedResult: JSPromise.Result = await withUnsafeContinuation { continuation in + chained.then( + success: { + continuation.resume(returning: .success($0)) + return JSValue.undefined + }, + failure: { + continuation.resume(returning: .failure($0)) + return JSValue.undefined + } + ) + } + if case .success(let val) = chainedResult { + check(val.number == 25, "Promise chaining works (10 * 2 + 5 = 25)") + } else { + check(false, "Promise chaining should succeed") + } + + // Summary + let console = JSObject.global.console + let totalTests = testsPassed + testsFailed + _ = console.log("TOTAL: \(totalTests) tests, \(testsPassed) passed, \(testsFailed) failed") + if testsFailed > 0 { + fatalError("\(testsFailed) test(s) failed") + } + } +} diff --git a/Examples/EmbeddedConcurrency/build.sh b/Examples/EmbeddedConcurrency/build.sh new file mode 100755 index 000000000..dd4055e95 --- /dev/null +++ b/Examples/EmbeddedConcurrency/build.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -euxo pipefail +package_dir="$(cd "$(dirname "$0")" && pwd)" +swift package --package-path "$package_dir" \ + --swift-sdk "${SWIFT_SDK_ID_wasm32_unknown_wasip1:-${SWIFT_SDK_ID:-wasm32-unknown-wasip1}}-embedded" \ + js --default-platform node -c release +node "$package_dir/run.mjs" diff --git a/Examples/EmbeddedConcurrency/run.mjs b/Examples/EmbeddedConcurrency/run.mjs new file mode 100644 index 000000000..cbd2353bf --- /dev/null +++ b/Examples/EmbeddedConcurrency/run.mjs @@ -0,0 +1,47 @@ +import { instantiate } from + "./.build/plugins/PackageToJS/outputs/Package/instantiate.js" +import { defaultNodeSetup } from + "./.build/plugins/PackageToJS/outputs/Package/platforms/node.js" + +const EXPECTED_TESTS = 6; +const TIMEOUT_MS = 30_000; + +// Intercept console.log to capture test output +const originalLog = console.log; +let totalLine = null; +let resolveTotal = null; +const totalPromise = new Promise((resolve) => { resolveTotal = resolve; }); +console.log = (...args) => { + const line = args.join(" "); + originalLog.call(console, ...args); + if (line.startsWith("TOTAL:")) { + totalLine = line; + resolveTotal(); + } +}; + +const options = await defaultNodeSetup(); +await instantiate(options); + +// Wait for the async main to complete (tests run via microtasks/setTimeout) +const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error("Timed out waiting for test results")), TIMEOUT_MS) +); +try { + await Promise.race([totalPromise, timeout]); +} catch (e) { + originalLog.call(console, `FAIL: ${e.message}`); + process.exit(1); +} + +if (!totalLine) { + originalLog.call(console, `FAIL: No test summary found — main() likely exited early`); + process.exit(1); +} +const match = totalLine.match(/TOTAL: (\d+) tests/); +const ran = match ? parseInt(match[1], 10) : 0; +if (ran !== EXPECTED_TESTS) { + originalLog.call(console, + `FAIL: Expected ${EXPECTED_TESTS} tests but only ${ran} ran`); + process.exit(1); +} From ef9ab92891b5d0da1179fd7617b4014777281d37 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 16 Mar 2026 23:32:58 +0000 Subject: [PATCH 07/14] EmbeddedConcurrency: don't use `-c release` for SwiftSyntax Remove the '-c release' option from the build command. --- Examples/EmbeddedConcurrency/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/EmbeddedConcurrency/build.sh b/Examples/EmbeddedConcurrency/build.sh index dd4055e95..8888ecaf3 100755 --- a/Examples/EmbeddedConcurrency/build.sh +++ b/Examples/EmbeddedConcurrency/build.sh @@ -3,5 +3,5 @@ set -euxo pipefail package_dir="$(cd "$(dirname "$0")" && pwd)" swift package --package-path "$package_dir" \ --swift-sdk "${SWIFT_SDK_ID_wasm32_unknown_wasip1:-${SWIFT_SDK_ID:-wasm32-unknown-wasip1}}-embedded" \ - js --default-platform node -c release + js --default-platform node node "$package_dir/run.mjs" From 451000df0816ba1ff133c6588a49b9fd738a760a Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 17 Mar 2026 01:00:15 +0000 Subject: [PATCH 08/14] Fix warnings with untyped throws, fix npm install error # Conflicts: # Examples/EmbeddedConcurrency/build.sh --- Examples/EmbeddedConcurrency/build.sh | 1 + Sources/JavaScriptEventLoop/JSSending.swift | 35 +++++++++++++++++++ .../JavaScriptEventLoop+ExecutorFactory.swift | 8 ++--- .../BasicObjects/JSTypedArray.swift | 4 +-- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Examples/EmbeddedConcurrency/build.sh b/Examples/EmbeddedConcurrency/build.sh index 8888ecaf3..d09c1d2a4 100755 --- a/Examples/EmbeddedConcurrency/build.sh +++ b/Examples/EmbeddedConcurrency/build.sh @@ -4,4 +4,5 @@ package_dir="$(cd "$(dirname "$0")" && pwd)" swift package --package-path "$package_dir" \ --swift-sdk "${SWIFT_SDK_ID_wasm32_unknown_wasip1:-${SWIFT_SDK_ID:-wasm32-unknown-wasip1}}-embedded" \ js --default-platform node +npm -C "$package_dir/.build/plugins/PackageToJS/outputs/Package" install node "$package_dir/run.mjs" diff --git a/Sources/JavaScriptEventLoop/JSSending.swift b/Sources/JavaScriptEventLoop/JSSending.swift index 7a3750c15..fb2fb1ddf 100644 --- a/Sources/JavaScriptEventLoop/JSSending.swift +++ b/Sources/JavaScriptEventLoop/JSSending.swift @@ -226,6 +226,32 @@ extension JSSending { /// - Parameter isolation: The actor isolation context for this call, used in Swift concurrency. /// - Returns: The received object of type `T`. /// - Throws: `JSSendingError` if the sending operation fails, or `JSException` if a JavaScript error occurs. + #if compiler(>=6.4) + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public func receive( + isolation: isolated (any Actor)? = #isolation, + file: StaticString = #file, + line: UInt = #line + ) async throws(JSException) -> T { + #if _runtime(_multithreaded) + let idInDestination = try await withCheckedThrowingContinuation { continuation in + let context = _JSSendingContext(continuation: continuation) + let idInSource = self.storage.idInSource + let transferring = self.storage.transferring ? [idInSource] : [] + swjs_request_sending_object( + idInSource, + transferring, + Int32(transferring.count), + self.storage.sourceTid, + Unmanaged.passRetained(context).toOpaque() + ) + } + return storage.construct(JSObject(id: idInDestination)) + #else + return storage.construct(storage.sourceObject) + #endif + } + #else @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func receive( isolation: isolated (any Actor)? = #isolation, @@ -250,6 +276,7 @@ extension JSSending { return storage.construct(storage.sourceObject) #endif } + #endif // 6.0 and below can't compile the following without a compiler crash. #if compiler(>=6.1) @@ -341,11 +368,19 @@ extension JSSending { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) private final class _JSSendingContext: Sendable { + #if compiler(>=6.4) + let continuation: CheckedContinuation + + init(continuation: CheckedContinuation) { + self.continuation = continuation + } + #else let continuation: CheckedContinuation init(continuation: CheckedContinuation) { self.continuation = continuation } + #endif } /// Error type representing failures during JavaScript object sending operations. diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift index 2b16e5110..d3fe26b15 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift @@ -40,9 +40,9 @@ extension JavaScriptEventLoop: SchedulingExecutor { tolerance: C.Duration?, clock: C ) { + #if !hasFeature(Embedded) let duration: Duration // Handle clocks we know - #if !hasFeature(Embedded) if let _ = clock as? ContinuousClock { duration = delay as! ContinuousClock.Duration } else if let _ = clock as? SuspendingClock { @@ -62,14 +62,14 @@ extension JavaScriptEventLoop: SchedulingExecutor { fatalError("Unsupported clock type; only ContinuousClock and SuspendingClock are supported") #endif } - #else - fatalError("SchedulingExecutor.enqueue is not supported in embedded mode") - #endif let milliseconds = Self.delayInMilliseconds(from: duration) self.enqueue( UnownedJob(job), withDelay: milliseconds ) + #else + fatalError("SchedulingExecutor.enqueue is not supported in embedded mode") + #endif } private static func delayInMilliseconds(from swiftDuration: Duration) -> Double { diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index dceecf5bf..f73558652 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -98,7 +98,7 @@ public final class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiter /// used as the return value for the `withUnsafeBytes(_:)` method. The /// argument is valid only for the duration of the closure's execution. /// - Returns: The return value, if any, of the `body` closure parameter. - public func withUnsafeBytes(_ body: (UnsafeBufferPointer) throws -> R) rethrows -> R { + public func withUnsafeBytes(_ body: (UnsafeBufferPointer) throws(E) -> R) throws(E) -> R { let buffer = UnsafeMutableBufferPointer.allocate(capacity: length) defer { buffer.deallocate() } copyMemory(to: buffer) @@ -121,7 +121,7 @@ public final class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiter /// argument is valid only for the duration of the closure's execution. /// - Returns: The return value, if any, of the `body`async closure parameter. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public func withUnsafeBytesAsync(_ body: (UnsafeBufferPointer) async throws -> R) async rethrows -> R { + public func withUnsafeBytesAsync(_ body: (UnsafeBufferPointer) async throws(E) -> R) async throws(E) -> R { let buffer = UnsafeMutableBufferPointer.allocate(capacity: length) defer { buffer.deallocate() } copyMemory(to: buffer) From be0d11df806a25adf9efc2a7534c50f9a723ad5b Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 17 Mar 2026 01:09:52 +0000 Subject: [PATCH 09/14] Fix formatting --- .../Sources/EmbeddedConcurrencyApp/App.swift | 5 +++-- Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Examples/EmbeddedConcurrency/Sources/EmbeddedConcurrencyApp/App.swift b/Examples/EmbeddedConcurrency/Sources/EmbeddedConcurrencyApp/App.swift index 07cb9de9e..bd792cc2d 100644 --- a/Examples/EmbeddedConcurrency/Sources/EmbeddedConcurrencyApp/App.swift +++ b/Examples/EmbeddedConcurrency/Sources/EmbeddedConcurrencyApp/App.swift @@ -6,8 +6,8 @@ import _Concurrency typealias DefaultExecutorFactory = JavaScriptEventLoop #endif -nonisolated(unsafe) var testsPassed = 0 -nonisolated(unsafe) var testsFailed = 0 +@MainActor var testsPassed = 0 +@MainActor var testsFailed = 0 @MainActor func check(_ condition: Bool, _ message: String) { @@ -23,6 +23,7 @@ func check(_ condition: Bool, _ message: String) { @main struct App { + @MainActor static func main() async throws(JSException) { JavaScriptEventLoop.installGlobalExecutor() diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index f73558652..0ad7b235a 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -121,7 +121,9 @@ public final class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiter /// argument is valid only for the duration of the closure's execution. /// - Returns: The return value, if any, of the `body`async closure parameter. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public func withUnsafeBytesAsync(_ body: (UnsafeBufferPointer) async throws(E) -> R) async throws(E) -> R { + public func withUnsafeBytesAsync( + _ body: (UnsafeBufferPointer) async throws(E) -> R + ) async throws(E) -> R { let buffer = UnsafeMutableBufferPointer.allocate(capacity: length) defer { buffer.deallocate() } copyMemory(to: buffer) From 89d4b5cf3cda188b6e891affd2a74dd7cd5de162 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 17 Mar 2026 09:34:58 +0000 Subject: [PATCH 10/14] Bump `build-examples` snapshots to 2026-03-14 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 22e5c2665..76b2b08e7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -167,7 +167,7 @@ jobs: - uses: actions/checkout@v6 - uses: ./.github/actions/install-swift with: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2026-02-02-a/swift-DEVELOPMENT-SNAPSHOT-2026-02-02-a-ubuntu22.04.tar.gz + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2026-03-14-a/swift-DEVELOPMENT-SNAPSHOT-2026-03-14-a-ubuntu22.04.tar.gz - uses: swiftwasm/setup-swiftwasm@v2 id: setup-wasm32-unknown-wasip1 with: { target: wasm32-unknown-wasip1 } From fc27b849aebcf5fbfda1782afea462803b89f3b1 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 17 Mar 2026 10:04:12 +0000 Subject: [PATCH 11/14] Use 2026-03-09 for `main` development snapshots --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 76b2b08e7..766f4ca4a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -167,7 +167,7 @@ jobs: - uses: actions/checkout@v6 - uses: ./.github/actions/install-swift with: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2026-03-14-a/swift-DEVELOPMENT-SNAPSHOT-2026-03-14-a-ubuntu22.04.tar.gz + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2026-03-09-a/swift-DEVELOPMENT-SNAPSHOT-2026-03-09-a-ubuntu22.04.tar.gz - uses: swiftwasm/setup-swiftwasm@v2 id: setup-wasm32-unknown-wasip1 with: { target: wasm32-unknown-wasip1 } From 79772bd58c7850ac767b9430b8fd2f88626441a1 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 17 Mar 2026 10:14:30 +0000 Subject: [PATCH 12/14] Add Swift version check before building examples --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 766f4ca4a..419c772a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -174,7 +174,9 @@ jobs: - uses: swiftwasm/setup-swiftwasm@v2 id: setup-wasm32-unknown-wasip1-threads with: { target: wasm32-unknown-wasip1-threads } - - run: ./Utilities/build-examples.sh + - run: | + swift --version + ./Utilities/build-examples.sh env: SWIFT_SDK_ID_wasm32_unknown_wasip1_threads: ${{ steps.setup-wasm32-unknown-wasip1-threads.outputs.swift-sdk-id }} SWIFT_SDK_ID_wasm32_unknown_wasip1: ${{ steps.setup-wasm32-unknown-wasip1.outputs.swift-sdk-id }} From 5d9aa130097e9108bd48a59ed37f4fb90aca533e Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 17 Mar 2026 10:38:24 +0000 Subject: [PATCH 13/14] Exercise `await` on `JSPromise/value` --- .../Sources/EmbeddedConcurrencyApp/App.swift | 30 +++++++++++++++++++ Examples/EmbeddedConcurrency/run.mjs | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Examples/EmbeddedConcurrency/Sources/EmbeddedConcurrencyApp/App.swift b/Examples/EmbeddedConcurrency/Sources/EmbeddedConcurrencyApp/App.swift index bd792cc2d..95b0a86aa 100644 --- a/Examples/EmbeddedConcurrency/Sources/EmbeddedConcurrencyApp/App.swift +++ b/Examples/EmbeddedConcurrency/Sources/EmbeddedConcurrencyApp/App.swift @@ -121,6 +121,36 @@ struct App { check(false, "Promise chaining should succeed") } + // Test 7: JSPromise.value await (with async resolution) + let promise2 = JSPromise(resolver: { resolve in + _ = JSObject.global.setTimeout!( + JSOneshotClosure { _ in + resolve(.success(JSValue.number(456))) + return .undefined + }, + 1 + ) + }) + let awaitedValue = try await promise2.value + check(awaitedValue.number == 456, "JSPromise.value await returns correct value") + + // Test 8: JSPromise.result await (with async resolution) + let promise3 = JSPromise(resolver: { resolve in + _ = JSObject.global.setTimeout!( + JSOneshotClosure { _ in + resolve(.success(JSValue.number(789))) + return .undefined + }, + 1 + ) + }) + let awaitedResult = await promise3.result + if case .success(let val) = awaitedResult { + check(val.number == 789, "JSPromise.result await resolves correctly") + } else { + check(false, "JSPromise.result await should succeed") + } + // Summary let console = JSObject.global.console let totalTests = testsPassed + testsFailed diff --git a/Examples/EmbeddedConcurrency/run.mjs b/Examples/EmbeddedConcurrency/run.mjs index cbd2353bf..2d755923c 100644 --- a/Examples/EmbeddedConcurrency/run.mjs +++ b/Examples/EmbeddedConcurrency/run.mjs @@ -3,7 +3,7 @@ import { instantiate } from import { defaultNodeSetup } from "./.build/plugins/PackageToJS/outputs/Package/platforms/node.js" -const EXPECTED_TESTS = 6; +const EXPECTED_TESTS = 8; const TIMEOUT_MS = 30_000; // Intercept console.log to capture test output From 48e891618362aaccdff8281b153adfd57400be35 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 17 Mar 2026 14:22:23 +0000 Subject: [PATCH 14/14] Address PR feedback --- .../JavaScriptEventLoop+ExecutorFactory.swift | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift index d3fe26b15..a9b6091e8 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift @@ -8,7 +8,7 @@ @_spi(ExperimentalCustomExecutors) @_spi(ExperimentalScheduling) import _Concurrency #else import _Concurrency -#endif +#endif // #if compiler(>=6.3) import _CJavaScriptKit #if compiler(>=6.3) @@ -40,7 +40,22 @@ extension JavaScriptEventLoop: SchedulingExecutor { tolerance: C.Duration?, clock: C ) { - #if !hasFeature(Embedded) + #if hasFeature(Embedded) + #if compiler(>=6.4) + // In Embedded Swift, ContinuousClock and SuspendingClock are unavailable. + // Hand-off the scheduling work to the Clock implementation for custom clocks. + clock.enqueue( + job, + on: self, + at: clock.now.advanced(by: delay), + tolerance: tolerance + ) + #else + fatalError( + "Delayed enqueue requires Swift 6.4+ in Embedded mode" + ) + #endif // #if compiler(>=6.4) (Embedded) + #else // #if hasFeature(Embedded) let duration: Duration // Handle clocks we know if let _ = clock as? ContinuousClock { @@ -60,16 +75,14 @@ extension JavaScriptEventLoop: SchedulingExecutor { return #else fatalError("Unsupported clock type; only ContinuousClock and SuspendingClock are supported") - #endif + #endif // #if compiler(>=6.4) (non-Embedded) } let milliseconds = Self.delayInMilliseconds(from: duration) self.enqueue( UnownedJob(job), withDelay: milliseconds ) - #else - fatalError("SchedulingExecutor.enqueue is not supported in embedded mode") - #endif + #endif // #if hasFeature(Embedded) } private static func delayInMilliseconds(from swiftDuration: Duration) -> Double { @@ -90,7 +103,6 @@ extension JavaScriptEventLoop: ExecutorFactory { JavaScriptEventLoop.shared.enqueue(job) } - #if !hasFeature(Embedded) func enqueue( _ job: consuming ExecutorJob, after delay: C.Duration, @@ -104,7 +116,6 @@ extension JavaScriptEventLoop: ExecutorFactory { clock: clock ) } - #endif func run() throws { try JavaScriptEventLoop.shared.run() } @@ -122,4 +133,4 @@ extension JavaScriptEventLoop: ExecutorFactory { } } -#endif // compiler(>=6.3) +#endif // #if compiler(>=6.3)