Skip to content

Commit 8a7ae30

Browse files
committed
Add Examples/EmbeddedConcurrency
1 parent 4c72b42 commit 8a7ae30

File tree

4 files changed

+208
-0
lines changed

4 files changed

+208
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// swift-tools-version:6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "EmbeddedConcurrency",
7+
dependencies: [
8+
.package(name: "JavaScriptKit", path: "../../")
9+
],
10+
targets: [
11+
.executableTarget(
12+
name: "EmbeddedConcurrencyApp",
13+
dependencies: [
14+
"JavaScriptKit",
15+
.product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
16+
],
17+
swiftSettings: [
18+
.enableExperimentalFeature("Extern"),
19+
.swiftLanguageMode(.v5),
20+
]
21+
)
22+
]
23+
)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
@preconcurrency import JavaScriptKit
2+
@preconcurrency import JavaScriptEventLoop
3+
import _Concurrency
4+
5+
#if compiler(>=6.3)
6+
typealias DefaultExecutorFactory = JavaScriptEventLoop
7+
#endif
8+
9+
nonisolated(unsafe) var testsPassed = 0
10+
nonisolated(unsafe) var testsFailed = 0
11+
12+
@MainActor
13+
func check(_ condition: Bool, _ message: String) {
14+
let console = JSObject.global.console
15+
if condition {
16+
testsPassed += 1
17+
_ = console.log("PASS: \(message)")
18+
} else {
19+
testsFailed += 1
20+
_ = console.log("FAIL: \(message)")
21+
}
22+
}
23+
24+
@main
25+
struct App {
26+
static func main() async throws(JSException) {
27+
JavaScriptEventLoop.installGlobalExecutor()
28+
29+
// Test 1: Basic async/await with checked continuation
30+
let value: Int = await withCheckedContinuation { cont in
31+
cont.resume(returning: 42)
32+
}
33+
check(value == 42, "withCheckedContinuation returns correct value")
34+
35+
// Test 2: Unsafe continuation
36+
let value2: Int = await withUnsafeContinuation { cont in
37+
cont.resume(returning: 7)
38+
}
39+
check(value2 == 7, "withUnsafeContinuation returns correct value")
40+
41+
// Test 3: JSPromise creation and .value await
42+
let promise = JSPromise(resolver: { resolve in
43+
resolve(.success(JSValue.number(123)))
44+
})
45+
let result: JSPromise.Result = await withUnsafeContinuation { continuation in
46+
promise.then(
47+
success: {
48+
continuation.resume(returning: .success($0))
49+
return JSValue.undefined
50+
},
51+
failure: {
52+
continuation.resume(returning: .failure($0))
53+
return JSValue.undefined
54+
}
55+
)
56+
}
57+
if case .success(let val) = result {
58+
check(val.number == 123, "JSPromise.value resolves correctly")
59+
} else {
60+
check(false, "JSPromise.value resolves correctly")
61+
}
62+
63+
// Test 4: setTimeout-based delay via JSPromise
64+
let startTime = JSObject.global.Date.now().number!
65+
let delayValue: Int = await withUnsafeContinuation { cont in
66+
_ = JSObject.global.setTimeout!(
67+
JSOneshotClosure { _ in
68+
cont.resume(returning: 42)
69+
return .undefined
70+
},
71+
100
72+
)
73+
}
74+
let elapsed = JSObject.global.Date.now().number! - startTime
75+
check(delayValue == 42 && elapsed >= 90, "setTimeout delay works (\(elapsed)ms elapsed)")
76+
77+
// Test 5: Multiple concurrent tasks (using withUnsafeContinuation to avoid nonisolated hop)
78+
var results: [Int] = []
79+
let task1 = Task { return 1 }
80+
let task2 = Task { return 2 }
81+
let task3 = Task { return 3 }
82+
let r1: Int = await withUnsafeContinuation { cont in
83+
Task { cont.resume(returning: await task1.value) }
84+
}
85+
let r2: Int = await withUnsafeContinuation { cont in
86+
Task { cont.resume(returning: await task2.value) }
87+
}
88+
let r3: Int = await withUnsafeContinuation { cont in
89+
Task { cont.resume(returning: await task3.value) }
90+
}
91+
results.append(r1)
92+
results.append(r2)
93+
results.append(r3)
94+
results.sort()
95+
check(results == [1, 2, 3], "Concurrent tasks all complete")
96+
97+
// Test 6: Promise chaining with .then
98+
let chained = JSPromise(resolver: { resolve in
99+
resolve(.success(JSValue.number(10)))
100+
}).then(success: { value in
101+
return JSValue.number(value.number! * 2)
102+
}).then(success: { value in
103+
return JSValue.number(value.number! + 5)
104+
})
105+
let chainedResult: JSPromise.Result = await withUnsafeContinuation { continuation in
106+
chained.then(
107+
success: {
108+
continuation.resume(returning: .success($0))
109+
return JSValue.undefined
110+
},
111+
failure: {
112+
continuation.resume(returning: .failure($0))
113+
return JSValue.undefined
114+
}
115+
)
116+
}
117+
if case .success(let val) = chainedResult {
118+
check(val.number == 25, "Promise chaining works (10 * 2 + 5 = 25)")
119+
} else {
120+
check(false, "Promise chaining should succeed")
121+
}
122+
123+
// Summary
124+
let console = JSObject.global.console
125+
let totalTests = testsPassed + testsFailed
126+
_ = console.log("TOTAL: \(totalTests) tests, \(testsPassed) passed, \(testsFailed) failed")
127+
if testsFailed > 0 {
128+
fatalError("\(testsFailed) test(s) failed")
129+
}
130+
}
131+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
set -euxo pipefail
3+
package_dir="$(cd "$(dirname "$0")" && pwd)"
4+
swift package --package-path "$package_dir" \
5+
--swift-sdk "${SWIFT_SDK_ID_wasm32_unknown_wasip1:-${SWIFT_SDK_ID:-wasm32-unknown-wasip1}}-embedded" \
6+
js --default-platform node -c release
7+
node "$package_dir/run.mjs"
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { instantiate } from
2+
"./.build/plugins/PackageToJS/outputs/Package/instantiate.js"
3+
import { defaultNodeSetup } from
4+
"./.build/plugins/PackageToJS/outputs/Package/platforms/node.js"
5+
6+
const EXPECTED_TESTS = 6;
7+
const TIMEOUT_MS = 30_000;
8+
9+
// Intercept console.log to capture test output
10+
const originalLog = console.log;
11+
let totalLine = null;
12+
let resolveTotal = null;
13+
const totalPromise = new Promise((resolve) => { resolveTotal = resolve; });
14+
console.log = (...args) => {
15+
const line = args.join(" ");
16+
originalLog.call(console, ...args);
17+
if (line.startsWith("TOTAL:")) {
18+
totalLine = line;
19+
resolveTotal();
20+
}
21+
};
22+
23+
const options = await defaultNodeSetup();
24+
await instantiate(options);
25+
26+
// Wait for the async main to complete (tests run via microtasks/setTimeout)
27+
const timeout = new Promise((_, reject) =>
28+
setTimeout(() => reject(new Error("Timed out waiting for test results")), TIMEOUT_MS)
29+
);
30+
try {
31+
await Promise.race([totalPromise, timeout]);
32+
} catch (e) {
33+
originalLog.call(console, `FAIL: ${e.message}`);
34+
process.exit(1);
35+
}
36+
37+
if (!totalLine) {
38+
originalLog.call(console, `FAIL: No test summary found — main() likely exited early`);
39+
process.exit(1);
40+
}
41+
const match = totalLine.match(/TOTAL: (\d+) tests/);
42+
const ran = match ? parseInt(match[1], 10) : 0;
43+
if (ran !== EXPECTED_TESTS) {
44+
originalLog.call(console,
45+
`FAIL: Expected ${EXPECTED_TESTS} tests but only ${ran} ran`);
46+
process.exit(1);
47+
}

0 commit comments

Comments
 (0)