Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattribute
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Sources/_CJavaScriptEventLoop/swift/* linguist-vendored
Sources/_CJavaScriptEventLoop/llvm/* linguist-vendored
2 changes: 1 addition & 1 deletion .swift-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
wasm-5.3.0-RELEASE
wasm-DEVELOPMENT-SNAPSHOT-2021-01-10-a
13 changes: 12 additions & 1 deletion Example/JavaScriptKitExample/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,16 @@ let package = Package(
),
],
dependencies: [.package(name: "JavaScriptKit", path: "../../")],
targets: [.target(name: "JavaScriptKitExample", dependencies: ["JavaScriptKit"])]
targets: [
.target(
name: "JavaScriptKitExample",
dependencies: [
.product(name: "JavaScriptKit", package: "JavaScriptKit"),
.product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
],
swiftSettings: [
.unsafeFlags(["-Xfrontend", "-enable-experimental-concurrency"]),
]
),
]
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import JavaScriptKit
import JavaScriptEventLoop

JavaScriptEventLoop.install()

let alert = JSObject.global.alert.function!
let document = JSObject.global.document
Expand All @@ -15,3 +18,15 @@ let listener = JSClosure { _ in
buttonElement.onclick = .object(listener)

_ = document.body.appendChild(buttonElement)

let fetch = JSObject.global.fetch.function!.async

func printZen() async {
let result = await try! fetch("https://api.github.com/zen").object!
let text = await try! result.asyncing.text!()
print(text)
}

JavaScriptEventLoop.runAsync {
await printZen()
}
2 changes: 2 additions & 0 deletions IntegrationTests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ benchmark: benchmark_setup run_benchmark
.PHONY: test
test: build_rt dist/PrimaryTests.wasm
node bin/primary-tests.js
concurrency_test: build_rt dist/ConcurrencyTests.wasm
node bin/concurrency-tests.js
9 changes: 9 additions & 0 deletions IntegrationTests/TestSuites/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ let package = Package(
dependencies: [.package(name: "JavaScriptKit", path: "../../")],
targets: [
.target(name: "PrimaryTests", dependencies: ["JavaScriptKit"]),
.target(
name: "ConcurrencyTests",
dependencies: [
.product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
],
swiftSettings: [
.unsafeFlags(["-Xfrontend", "-enable-experimental-concurrency"]),
]
),
.target(name: "BenchmarkTests", dependencies: ["JavaScriptKit"]),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import JavaScriptKit

var printTestNames = false
// Uncomment the next line to print the name of each test suite before running it.
// This will make it easier to debug any errors that occur on the JS side.
//printTestNames = true

func test(_ name: String, testBlock: () throws -> Void) throws {
if printTestNames { print(name) }
do {
try testBlock()
} catch {
print("Error in \(name)")
print(error)
throw error
}
}

func asyncTest(_ name: String, testBlock: () async throws -> Void) async throws -> Void {
if printTestNames { print(name) }
do {
await try testBlock()
} catch {
print("Error in \(name)")
print(error)
throw error
}
}

struct MessageError: Error {
let message: String
let file: StaticString
let line: UInt
let column: UInt
init(_ message: String, file: StaticString, line: UInt, column: UInt) {
self.message = message
self.file = file
self.line = line
self.column = column
}
}

func expectEqual<T: Equatable>(
_ lhs: T, _ rhs: T,
file: StaticString = #file, line: UInt = #line, column: UInt = #column
) throws {
if lhs != rhs {
throw MessageError("Expect to be equal \"\(lhs)\" and \"\(rhs)\"", file: file, line: line, column: column)
}
}

func expectCast<T, U>(
_ value: T, to type: U.Type = U.self,
file: StaticString = #file, line: UInt = #line, column: UInt = #column
) throws -> U {
guard let value = value as? U else {
throw MessageError("Expect \"\(value)\" to be \(U.self)", file: file, line: line, column: column)
}
return value
}

func expectObject(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSObject {
switch value {
case let .object(ref): return ref
default:
throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column)
}
}

func expectArray(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSArray {
guard let array = value.array else {
throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column)
}
return array
}

func expectFunction(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSFunction {
switch value {
case let .function(ref): return ref
default:
throw MessageError("Type of \(value) should be \"function\"", file: file, line: line, column: column)
}
}

func expectBoolean(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Bool {
switch value {
case let .boolean(bool): return bool
default:
throw MessageError("Type of \(value) should be \"boolean\"", file: file, line: line, column: column)
}
}

func expectNumber(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Double {
switch value {
case let .number(number): return number
default:
throw MessageError("Type of \(value) should be \"number\"", file: file, line: line, column: column)
}
}

func expectString(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> String {
switch value {
case let .string(string): return String(string)
default:
throw MessageError("Type of \(value) should be \"string\"", file: file, line: line, column: column)
}
}

func expectAsyncThrow<T>(_ body: @autoclosure () async throws -> T, file: StaticString = #file, line: UInt = #line, column: UInt = #column) async throws -> Error {
do {
_ = await try body()
} catch {
return error
}
throw MessageError("Expect to throw an exception", file: file, line: line, column: column)
}

func expectNotNil<T>(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws {
switch value {
case .some: return
case .none:
throw MessageError("Expect a non-nil value", file: file, line: line, column: column)
}
}
40 changes: 40 additions & 0 deletions IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import JavaScriptEventLoop
import JavaScriptKit

JavaScriptEventLoop.install()

try JavaScriptEventLoop.runAsync {
struct E: Error, Equatable {
let value: Int
}

await try asyncTest("Task.runDetached value") {
let handle = Task.runDetached { 1 }
await try expectEqual(handle.get(), 1)
}

await try asyncTest("Task.runDetached throws") {
let handle = Task.runDetached {
throw E(value: 2)
}
let error = await try expectAsyncThrow(await handle.get())
let e = try expectCast(error, to: E.self)
try expectEqual(e, E(value: 2))
}

await try asyncTest("await resolved Promise") {
let p = JSPromise(resolver: { resolve in
resolve(.success(1))
})
await try expectEqual(p.await(), 1)
}

await try asyncTest("await rejected Promise") {
let p = JSPromise(resolver: { resolve in
resolve(.failure(.number(3)))
})
let error = await try expectAsyncThrow(await p.await())
let jsValue = try expectCast(error, to: JSValue.self)
try expectEqual(jsValue, 3)
}
}
15 changes: 15 additions & 0 deletions IntegrationTests/bin/concurrency-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { startWasiTask } = require("../lib");

global.fetch = require('node-fetch');
global.sleep = function () {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}

startWasiTask("./dist/ConcurrencyTests.wasm").catch((err) => {
console.log(err);
process.exit(1);
});
28 changes: 26 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
// swift-tools-version:5.2
// swift-tools-version:5.3

import PackageDescription

let package = Package(
name: "JavaScriptKit",
products: [
.library(name: "JavaScriptKit", targets: ["JavaScriptKit"]),
.library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]),
],
targets: [
.target(
name: "JavaScriptKit",
dependencies: ["_CJavaScriptKit"]
),
.target(
name: "JavaScriptEventLoop",
dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"],
swiftSettings: [
.unsafeFlags(["-Xfrontend", "-enable-experimental-concurrency"]),
]
),
.target(name: "_CJavaScriptKit"),
]
.target(
name: "_CJavaScriptEventLoop",
dependencies: ["_CJavaScriptKit"],
exclude: [
"README", "LICENSE-llvm", "LICENSE-swift", "scripts",
"include/swift/ABI/MetadataKind.def",
"include/swift/ABI/ValueWitness.def",
"include/swift/AST/ReferenceStorage.def",
"include/swift/Demangling/DemangleNodes.def",
"include/swift/Demangling/ValueWitnessMangling.def",
],
linkerSettings: [
.linkedLibrary("swift_Concurrency", .when(platforms: [.wasi])),
]
),
],
cxxLanguageStandard: .cxx14
)
87 changes: 87 additions & 0 deletions Sources/JavaScriptEventLoop/JSAsyncFunction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import JavaScriptKit

/// A `JSFunction` wrapper that enables async-function calls.
/// Exceptions produced by JavaScript functions will be thrown as `JSValue`.
///
/// ```swift
/// let fetch = JSObject.global.fetch.function!.async
/// let result = await try! fetch("https://api.github.com/zen")
/// ```
public class JSAsyncFunction {
private let base: JSFunction
public init(_ base: JSFunction) {
self.base = base
}

/// Call this function with given `arguments` and binding given `this` as context.
/// - Parameters:
/// - this: The value to be passed as the `this` parameter to this function.
/// - arguments: Arguments to be passed to this function.
/// - Returns: The result of this call.
@discardableResult
public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) async throws -> JSValue {
let result = base.callAsFunction(this: this, arguments: arguments)
guard let object = result.object, let promise = JSPromise(object) else {
fatalError("'\(result)' should be Promise object")
}
return await try promise.await()
}

/// A variadic arguments version of `callAsFunction`.
@discardableResult
public func callAsFunction(this: JSObject? = nil, _ arguments: ConvertibleToJSValue...) async throws -> JSValue {
await try callAsFunction(this: this, arguments: arguments)
}
}

public extension JSFunction {
/// A modifier to call this function as a async function
///
/// ```swift
/// let fetch = JSObject.global.fetch.function!.async
/// let result = await try! fetch("https://api.github.com/zen")
/// ```
var `async`: JSAsyncFunction {
JSAsyncFunction(self)
}
}

/// A `JSObject` wrapper that enables async method calls capturing `this`.
/// Exceptions produced by JavaScript functions will be thrown as `JSValue`.
@dynamicMemberLookup
public class JSAsyncingObject {
private let base: JSObject
public init(_ base: JSObject) {
self.base = base
}

/// Returns the `name` member method binding this object as `this` context.
/// - Parameter name: The name of this object's member to access.
/// - Returns: The `name` member method binding this object as `this` context.
public subscript(_ name: String) -> ((ConvertibleToJSValue...) async throws -> JSValue)? {
guard let function = base[name].function?.async else { return nil }
return { [base] (arguments: ConvertibleToJSValue...) in
await try function(this: base, arguments: arguments)
}
}

/// A convenience method of `subscript(_ name: String) -> ((ConvertibleToJSValue...) throws -> JSValue)?`
/// to access the member through Dynamic Member Lookup.
public subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) async throws -> JSValue)? {
self[name]
}
}


public extension JSObject {

/// A modifier to call methods as async methods capturing `this`
///
/// ```swift
/// let fetch = JSObject.global.fetch.function!.async
/// let result = await try! fetch("https://api.github.com/zen")
/// ```
var asyncing: JSAsyncingObject {
JSAsyncingObject(self)
}
}
Loading