Skip to content

Commit ab3da74

Browse files
authored
Gracefully handle unavailable JSBridgedClass (#190)
Force unwrapping in `class var constructor` may crash for classes that are unavailable in the current environment. For example, after swiftwasm/WebAPIKit#38 was merged, it led to crashes in `getCanvas` calls due to these optional casts relying on `class var constructor` always succeeding: ```swift public static func construct(from value: JSValue) -> Self? { if let canvasRenderingContext2D: CanvasRenderingContext2D = value.fromJSValue() { return .canvasRenderingContext2D(canvasRenderingContext2D) } if let gpuCanvasContext: GPUCanvasContext = value.fromJSValue() { return .gpuCanvasContext(gpuCanvasContext) } if let imageBitmapRenderingContext: ImageBitmapRenderingContext = value.fromJSValue() { return .imageBitmapRenderingContext(imageBitmapRenderingContext) } if let webGL2RenderingContext: WebGL2RenderingContext = value.fromJSValue() { return .webGL2RenderingContext(webGL2RenderingContext) } if let webGLRenderingContext: WebGLRenderingContext = value.fromJSValue() { return .webGLRenderingContext(webGLRenderingContext) } return nil } ``` `if let gpuCanvasContext: GPUCanvasContext = value.fromJSValue()` branch crashes on browsers that don't have `GPUCanvasContext` enabled. As we currently don't have a better way to handle unavailable features, I propose making the result type of `static var constructor` requirement optional. This means you can still declare classes that are unavailable in the host JS environment. Conditional type casts are also available as they were, they will just always return `nil`, and initializers for these classes will return `nil` as well.
1 parent e022311 commit ab3da74

File tree

6 files changed

+98
-90
lines changed

6 files changed

+98
-90
lines changed

Sources/JavaScriptKit/BasicObjects/JSArray.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/// A wrapper around [the JavaScript Array class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)
22
/// that exposes its properties in a type-safe and Swifty way.
33
public class JSArray: JSBridgedClass {
4-
public static let constructor = JSObject.global.Array.function!
4+
public static let constructor = JSObject.global.Array.function
55

66
static func isArray(_ object: JSObject) -> Bool {
7-
constructor.isArray!(object).boolean!
7+
constructor!.isArray!(object).boolean!
88
}
99

1010
public let jsObject: JSObject
@@ -94,8 +94,8 @@ private func getObjectValuesLength(_ object: JSObject) -> Int {
9494
return Int(values.length.number!)
9595
}
9696

97-
extension JSValue {
98-
public var array: JSArray? {
97+
public extension JSValue {
98+
var array: JSArray? {
9999
object.flatMap(JSArray.init)
100100
}
101101
}

Sources/JavaScriptKit/BasicObjects/JSDate.swift

+30-30
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
1-
/** A wrapper around the [JavaScript Date
2-
class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) that
3-
exposes its properties in a type-safe way. This doesn't 100% match the JS API, for example
4-
`getMonth`/`setMonth` etc accessor methods are converted to properties, but the rest of it matches
5-
in the naming. Parts of the JavaScript `Date` API that are not consistent across browsers and JS
6-
implementations are not exposed in a type-safe manner, you should access the underlying `jsObject`
7-
property if you need those.
8-
*/
1+
/** A wrapper around the [JavaScript Date
2+
class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) that
3+
exposes its properties in a type-safe way. This doesn't 100% match the JS API, for example
4+
`getMonth`/`setMonth` etc accessor methods are converted to properties, but the rest of it matches
5+
in the naming. Parts of the JavaScript `Date` API that are not consistent across browsers and JS
6+
implementations are not exposed in a type-safe manner, you should access the underlying `jsObject`
7+
property if you need those.
8+
*/
99
public final class JSDate: JSBridgedClass {
1010
/// The constructor function used to create new `Date` objects.
11-
public static let constructor = JSObject.global.Date.function!
11+
public static let constructor = JSObject.global.Date.function
1212

1313
/// The underlying JavaScript `Date` object.
1414
public let jsObject: JSObject
1515

1616
/** Creates a new instance of the JavaScript `Date` class with a given amount of milliseconds
17-
that passed since midnight 01 January 1970 UTC.
18-
*/
17+
that passed since midnight 01 January 1970 UTC.
18+
*/
1919
public init(millisecondsSinceEpoch: Double? = nil) {
2020
if let milliseconds = millisecondsSinceEpoch {
21-
jsObject = Self.constructor.new(milliseconds)
21+
jsObject = Self.constructor!.new(milliseconds)
2222
} else {
23-
jsObject = Self.constructor.new()
23+
jsObject = Self.constructor!.new()
2424
}
2525
}
2626

27-
/** According to the standard, `monthIndex` is zero-indexed, where `11` is December. `day`
28-
represents a day of the month starting at `1`.
29-
*/
27+
/** According to the standard, `monthIndex` is zero-indexed, where `11` is December. `day`
28+
represents a day of the month starting at `1`.
29+
*/
3030
public init(
3131
year: Int,
3232
monthIndex: Int,
@@ -36,7 +36,7 @@ public final class JSDate: JSBridgedClass {
3636
seconds: Int = 0,
3737
milliseconds: Int = 0
3838
) {
39-
jsObject = Self.constructor.new(year, monthIndex, day, hours, minutes, seconds, milliseconds)
39+
jsObject = Self.constructor!.new(year, monthIndex, day, hours, minutes, seconds, milliseconds)
4040
}
4141

4242
public init(unsafelyWrapping jsObject: JSObject) {
@@ -198,7 +198,7 @@ public final class JSDate: JSBridgedClass {
198198
Int(jsObject.getTimezoneOffset!().number!)
199199
}
200200

201-
/// Returns a string conforming to ISO 8601 that contains date and time, e.g.
201+
/// Returns a string conforming to ISO 8601 that contains date and time, e.g.
202202
/// `"2020-09-15T08:56:54.811Z"`.
203203
public func toISOString() -> String {
204204
jsObject.toISOString!().string!
@@ -214,25 +214,25 @@ public final class JSDate: JSBridgedClass {
214214
jsObject.toLocaleTimeString!().string!
215215
}
216216

217-
/** Returns a string formatted according to
218-
[rfc7231](https://tools.ietf.org/html/rfc7231#section-7.1.1.1) and modified according to
219-
[ecma-262](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-date.prototype.toutcstring),
220-
e.g. `Tue, 15 Sep 2020 09:04:40 GMT`.
221-
*/
217+
/** Returns a string formatted according to
218+
[rfc7231](https://tools.ietf.org/html/rfc7231#section-7.1.1.1) and modified according to
219+
[ecma-262](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-date.prototype.toutcstring),
220+
e.g. `Tue, 15 Sep 2020 09:04:40 GMT`.
221+
*/
222222
public func toUTCString() -> String {
223223
jsObject.toUTCString!().string!
224224
}
225225

226-
/** Number of milliseconds since midnight 01 January 1970 UTC to the present moment ignoring
227-
leap seconds.
228-
*/
226+
/** Number of milliseconds since midnight 01 January 1970 UTC to the present moment ignoring
227+
leap seconds.
228+
*/
229229
public static func now() -> Double {
230-
constructor.now!().number!
230+
constructor!.now!().number!
231231
}
232232

233-
/** Number of milliseconds since midnight 01 January 1970 UTC to the given date ignoring leap
234-
seconds.
235-
*/
233+
/** Number of milliseconds since midnight 01 January 1970 UTC to the given date ignoring leap
234+
seconds.
235+
*/
236236
public func valueOf() -> Double {
237237
jsObject.valueOf!().number!
238238
}

Sources/JavaScriptKit/BasicObjects/JSError.swift

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
/** A wrapper around [the JavaScript Error
2-
class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) that
3-
exposes its properties in a type-safe way.
4-
*/
1+
/** A wrapper around [the JavaScript Error
2+
class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) that
3+
exposes its properties in a type-safe way.
4+
*/
55
public final class JSError: Error, JSBridgedClass {
66
/// The constructor function used to create new JavaScript `Error` objects.
7-
public static let constructor = JSObject.global.Error.function!
7+
public static let constructor = JSObject.global.Error.function
88

99
/// The underlying JavaScript `Error` object.
1010
public let jsObject: JSObject
1111

1212
/// Creates a new instance of the JavaScript `Error` class with a given message.
1313
public init(message: String) {
14-
jsObject = Self.constructor.new([message])
14+
jsObject = Self.constructor!.new([message])
1515
}
1616

1717
public init(unsafelyWrapping jsObject: JSObject) {

Sources/JavaScriptKit/BasicObjects/JSPromise.swift

+34-32
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/** A wrapper around [the JavaScript `Promise` class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)
2-
that exposes its functions in a type-safe and Swifty way. The `JSPromise` API is generic over both
3-
`Success` and `Failure` types, which improves compatibility with other statically-typed APIs such
4-
as Combine. If you don't know the exact type of your `Success` value, you should use `JSValue`, e.g.
5-
`JSPromise<JSValue, JSError>`. In the rare case, where you can't guarantee that the error thrown
6-
is of actual JavaScript `Error` type, you should use `JSPromise<JSValue, JSValue>`.
2+
that exposes its functions in a type-safe and Swifty way. The `JSPromise` API is generic over both
3+
`Success` and `Failure` types, which improves compatibility with other statically-typed APIs such
4+
as Combine. If you don't know the exact type of your `Success` value, you should use `JSValue`, e.g.
5+
`JSPromise<JSValue, JSError>`. In the rare case, where you can't guarantee that the error thrown
6+
is of actual JavaScript `Error` type, you should use `JSPromise<JSValue, JSValue>`.
77

8-
This doesn't 100% match the JavaScript API, as `then` overload with two callbacks is not available.
9-
It's impossible to unify success and failure types from both callbacks in a single returned promise
10-
without type erasure. You should chain `then` and `catch` in those cases to avoid type erasure.
11-
*/
8+
This doesn't 100% match the JavaScript API, as `then` overload with two callbacks is not available.
9+
It's impossible to unify success and failure types from both callbacks in a single returned promise
10+
without type erasure. You should chain `then` and `catch` in those cases to avoid type erasure.
11+
*/
1212
public final class JSPromise: JSBridgedClass {
1313
/// The underlying JavaScript `Promise` object.
1414
public let jsObject: JSObject
@@ -18,35 +18,35 @@ public final class JSPromise: JSBridgedClass {
1818
.object(jsObject)
1919
}
2020

21-
public static var constructor: JSFunction {
21+
public static var constructor: JSFunction? {
2222
JSObject.global.Promise.function!
2323
}
2424

2525
/// This private initializer assumes that the passed object is a JavaScript `Promise`
2626
public init(unsafelyWrapping object: JSObject) {
27-
self.jsObject = object
27+
jsObject = object
2828
}
2929

3030
/** Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `jsObject`
31-
is not an instance of JavaScript `Promise`, this initializer will return `nil`.
32-
*/
31+
is not an instance of JavaScript `Promise`, this initializer will return `nil`.
32+
*/
3333
public convenience init?(_ jsObject: JSObject) {
3434
self.init(from: jsObject)
3535
}
3636

3737
/** Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `value`
38-
is not an object and is not an instance of JavaScript `Promise`, this function will
39-
return `nil`.
40-
*/
38+
is not an object and is not an instance of JavaScript `Promise`, this function will
39+
return `nil`.
40+
*/
4141
public static func construct(from value: JSValue) -> Self? {
4242
guard case let .object(jsObject) = value else { return nil }
43-
return Self.init(jsObject)
43+
return Self(jsObject)
4444
}
4545

4646
/** Creates a new `JSPromise` instance from a given `resolver` closure. `resolver` takes
47-
two closure that your code should call to either resolve or reject this `JSPromise` instance.
48-
*/
49-
public convenience init(resolver: @escaping (@escaping (Result<JSValue, JSValue>) -> ()) -> ()) {
47+
two closure that your code should call to either resolve or reject this `JSPromise` instance.
48+
*/
49+
public convenience init(resolver: @escaping (@escaping (Result<JSValue, JSValue>) -> Void) -> Void) {
5050
let closure = JSOneshotClosure { arguments in
5151
// The arguments are always coming from the `Promise` constructor, so we should be
5252
// safe to assume their type here
@@ -63,19 +63,19 @@ public final class JSPromise: JSBridgedClass {
6363
}
6464
return .undefined
6565
}
66-
self.init(unsafelyWrapping: Self.constructor.new(closure))
66+
self.init(unsafelyWrapping: Self.constructor!.new(closure))
6767
}
6868

6969
public static func resolve(_ value: ConvertibleToJSValue) -> JSPromise {
70-
self.init(unsafelyWrapping: Self.constructor.resolve!(value).object!)
70+
self.init(unsafelyWrapping: Self.constructor!.resolve!(value).object!)
7171
}
7272

7373
public static func reject(_ reason: ConvertibleToJSValue) -> JSPromise {
74-
self.init(unsafelyWrapping: Self.constructor.reject!(reason).object!)
74+
self.init(unsafelyWrapping: Self.constructor!.reject!(reason).object!)
7575
}
7676

7777
/** Schedules the `success` closure to be invoked on sucessful completion of `self`.
78-
*/
78+
*/
7979
@discardableResult
8080
public func then(success: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise {
8181
let closure = JSOneshotClosure {
@@ -85,10 +85,12 @@ public final class JSPromise: JSBridgedClass {
8585
}
8686

8787
/** Schedules the `success` closure to be invoked on sucessful completion of `self`.
88-
*/
88+
*/
8989
@discardableResult
90-
public func then(success: @escaping (JSValue) -> ConvertibleToJSValue,
91-
failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise {
90+
public func then(
91+
success: @escaping (JSValue) -> ConvertibleToJSValue,
92+
failure: @escaping (JSValue) -> ConvertibleToJSValue
93+
) -> JSPromise {
9294
let successClosure = JSOneshotClosure {
9395
success($0[0]).jsValue
9496
}
@@ -99,7 +101,7 @@ public final class JSPromise: JSBridgedClass {
99101
}
100102

101103
/** Schedules the `failure` closure to be invoked on rejected completion of `self`.
102-
*/
104+
*/
103105
@discardableResult
104106
public func `catch`(failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise {
105107
let closure = JSOneshotClosure {
@@ -108,11 +110,11 @@ public final class JSPromise: JSBridgedClass {
108110
return .init(unsafelyWrapping: jsObject.catch!(closure).object!)
109111
}
110112

111-
/** Schedules the `failure` closure to be invoked on either successful or rejected completion of
112-
`self`.
113-
*/
113+
/** Schedules the `failure` closure to be invoked on either successful or rejected completion of
114+
`self`.
115+
*/
114116
@discardableResult
115-
public func finally(successOrFailure: @escaping () -> ()) -> JSPromise {
117+
public func finally(successOrFailure: @escaping () -> Void) -> JSPromise {
116118
let closure = JSOneshotClosure { _ in
117119
successOrFailure()
118120
return .undefined

Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift

+14-8
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ public protocol TypedArrayElement: ConvertibleToJSValue, ConstructibleFromJSValu
1313
/// A wrapper around all JavaScript [TypedArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/TypedArray)
1414
/// classes that exposes their properties in a type-safe way.
1515
public class JSTypedArray<Element>: JSBridgedClass, ExpressibleByArrayLiteral where Element: TypedArrayElement {
16-
public class var constructor: JSFunction { Element.typedArrayClass }
16+
public class var constructor: JSFunction? { Element.typedArrayClass }
1717
public var jsObject: JSObject
1818

1919
public subscript(_ index: Int) -> Element {
2020
get {
2121
return Element.construct(from: jsObject[index])!
2222
}
2323
set {
24-
self.jsObject[index] = newValue.jsValue
24+
jsObject[index] = newValue.jsValue
2525
}
2626
}
2727

@@ -30,22 +30,23 @@ public class JSTypedArray<Element>: JSBridgedClass, ExpressibleByArrayLiteral wh
3030
///
3131
/// - Parameter length: The number of elements that will be allocated.
3232
public init(length: Int) {
33-
jsObject = Self.constructor.new(length)
33+
jsObject = Self.constructor!.new(length)
3434
}
3535

36-
required public init(unsafelyWrapping jsObject: JSObject) {
36+
public required init(unsafelyWrapping jsObject: JSObject) {
3737
self.jsObject = jsObject
3838
}
3939

40-
required public convenience init(arrayLiteral elements: Element...) {
40+
public required convenience init(arrayLiteral elements: Element...) {
4141
self.init(elements)
4242
}
43+
4344
/// Initialize a new instance of TypedArray in JavaScript environment with given elements.
4445
///
4546
/// - Parameter array: The array that will be copied to create a new instance of TypedArray
4647
public convenience init(_ array: [Element]) {
4748
let jsArrayRef = array.withUnsafeBufferPointer { ptr in
48-
_create_typed_array(Self.constructor.id, ptr.baseAddress!, Int32(array.count))
49+
_create_typed_array(Self.constructor!.id, ptr.baseAddress!, Int32(array.count))
4950
}
5051
self.init(unsafelyWrapping: JSObject(id: jsArrayRef))
5152
}
@@ -80,7 +81,7 @@ public class JSTypedArray<Element>: JSBridgedClass, ExpressibleByArrayLiteral wh
8081
let rawBuffer = malloc(bytesLength)!
8182
defer { free(rawBuffer) }
8283
_load_typed_array(jsObject.id, rawBuffer.assumingMemoryBound(to: UInt8.self))
83-
let length = lengthInBytes / MemoryLayout<Element>.size
84+
let length = lengthInBytes / MemoryLayout<Element>.size
8485
let boundPtr = rawBuffer.bindMemory(to: Element.self, capacity: length)
8586
let bufferPtr = UnsafeBufferPointer<Element>(start: boundPtr, count: length)
8687
let result = try body(bufferPtr)
@@ -105,6 +106,7 @@ extension Int: TypedArrayElement {
105106
public static var typedArrayClass: JSFunction =
106107
valueForBitWidth(typeName: "Int", bitWidth: Int.bitWidth, when32: JSObject.global.Int32Array).function!
107108
}
109+
108110
extension UInt: TypedArrayElement {
109111
public static var typedArrayClass: JSFunction =
110112
valueForBitWidth(typeName: "UInt", bitWidth: Int.bitWidth, when32: JSObject.global.Uint32Array).function!
@@ -113,31 +115,35 @@ extension UInt: TypedArrayElement {
113115
extension Int8: TypedArrayElement {
114116
public static var typedArrayClass = JSObject.global.Int8Array.function!
115117
}
118+
116119
extension UInt8: TypedArrayElement {
117120
public static var typedArrayClass = JSObject.global.Uint8Array.function!
118121
}
119122

120123
public class JSUInt8ClampedArray: JSTypedArray<UInt8> {
121-
public class override var constructor: JSFunction { JSObject.global.Uint8ClampedArray.function! }
124+
override public class var constructor: JSFunction? { JSObject.global.Uint8ClampedArray.function! }
122125
}
123126

124127
extension Int16: TypedArrayElement {
125128
public static var typedArrayClass = JSObject.global.Int16Array.function!
126129
}
130+
127131
extension UInt16: TypedArrayElement {
128132
public static var typedArrayClass = JSObject.global.Uint16Array.function!
129133
}
130134

131135
extension Int32: TypedArrayElement {
132136
public static var typedArrayClass = JSObject.global.Int32Array.function!
133137
}
138+
134139
extension UInt32: TypedArrayElement {
135140
public static var typedArrayClass = JSObject.global.Uint32Array.function!
136141
}
137142

138143
extension Float32: TypedArrayElement {
139144
public static var typedArrayClass = JSObject.global.Float32Array.function!
140145
}
146+
141147
extension Float64: TypedArrayElement {
142148
public static var typedArrayClass = JSObject.global.Float64Array.function!
143149
}

0 commit comments

Comments
 (0)