1 import Dispatch 2 3 /** 4 Holds a reference to the module instance and caches its definition. 5 */ 6 public final class ModuleHolder { 7 /** 8 Instance of the module. 9 */ 10 private(set) var module: AnyModule 11 12 /** 13 A weak reference to the app context. 14 */ 15 private(set) weak var appContext: AppContext? 16 17 /** 18 JavaScript object that represents the module instance in the runtime. 19 */ 20 public internal(set) lazy var javaScriptObject: JavaScriptObject? = createJavaScriptModuleObject() 21 22 /** 23 Caches the definition of the module type. 24 */ 25 let definition: ModuleDefinition 26 27 /** 28 Returns `definition.name` if not empty, otherwise falls back to the module type name. 29 */ 30 var name: String { 31 return definition.name.isEmpty ? String(describing: type(of: module)) : definition.name 32 } 33 34 /** 35 Shortcut to get the underlying view manager definition. 36 */ 37 var viewManager: ViewManagerDefinition? { 38 return definition.viewManager 39 } 40 41 /** 42 Number of JavaScript listeners attached to the module. 43 */ 44 var listenersCount: Int = 0 45 46 init(appContext: AppContext, module: AnyModule) { 47 self.appContext = appContext 48 self.module = module 49 self.definition = module.definition() 50 post(event: .moduleCreate) 51 } 52 53 // MARK: Constants 54 55 /** 56 Merges all `constants` definitions into one dictionary. 57 */ 58 func getConstants() -> [String: Any?] { 59 return definition.getConstants() 60 } 61 62 // MARK: Calling functions 63 64 func call(function functionName: String, args: [Any], _ callback: @escaping (FunctionCallResult) -> () = { _ in }) { 65 guard let appContext else { 66 callback(.failure(Exceptions.AppContextLost())) 67 return 68 } 69 guard let function = definition.functions[functionName] else { 70 callback(.failure(FunctionNotFoundException((functionName: functionName, moduleName: self.name)))) 71 return 72 } 73 function.call(by: self, withArguments: args, appContext: appContext, callback: callback) 74 } 75 76 @discardableResult 77 func callSync(function functionName: String, args: [Any]) -> Any? { 78 guard let appContext, let function = definition.functions[functionName] as? AnySyncFunctionComponent else { 79 return nil 80 } 81 do { 82 let arguments = try cast(arguments: args, forFunction: function, appContext: appContext) 83 let result = try function.call(by: self, withArguments: arguments, appContext: appContext) 84 85 if let result = result as? SharedObject { 86 let jsObject = SharedObjectRegistry.ensureSharedJavaScriptObject(runtime: try appContext.runtime, nativeObject: result) 87 return jsObject 88 } 89 return result 90 } catch { 91 return error 92 } 93 } 94 95 // MARK: JavaScript Module Object 96 97 /** 98 Creates the JavaScript object that will be used to communicate with the native module. 99 The object is prefilled with module's constants and functions. 100 JavaScript can access it through `global.expo.modules[moduleName]`. 101 - Note: The object will be `nil` when the runtime is unavailable (e.g. remote debugger is enabled). 102 */ 103 private func createJavaScriptModuleObject() -> JavaScriptObject? { 104 // It might be impossible to create any object at the moment (e.g. remote debugging, app context destroyed) 105 guard let appContext else { 106 return nil 107 } 108 do { 109 log.info("Creating JS object for module '\(name)'") 110 return try definition.build(appContext: appContext) 111 } catch { 112 log.error("Building the module object failed: \(error)") 113 return nil 114 } 115 } 116 117 // MARK: Listening to native events 118 119 func listeners(forEvent event: EventName) -> [EventListener] { 120 return definition.eventListeners.filter { 121 $0.name == event 122 } 123 } 124 125 func post(event: EventName) { 126 listeners(forEvent: event).forEach { 127 try? $0.call(module, nil) 128 } 129 } 130 131 func post<PayloadType>(event: EventName, payload: PayloadType?) { 132 listeners(forEvent: event).forEach { 133 try? $0.call(module, payload) 134 } 135 } 136 137 // MARK: JavaScript events 138 139 /** 140 Modifies module's listeners count and calls `onStartObserving` or `onStopObserving` accordingly. 141 */ 142 func modifyListenersCount(_ count: Int) { 143 guard let appContext else { 144 return 145 } 146 if count > 0 && listenersCount == 0 { 147 definition.functions["startObserving"]?.call(withArguments: [], appContext: appContext) 148 } else if count < 0 && listenersCount + count <= 0 { 149 definition.functions["stopObserving"]?.call(withArguments: [], appContext: appContext) 150 } 151 listenersCount = max(0, listenersCount + count) 152 } 153 154 // MARK: Deallocation 155 156 deinit { 157 post(event: .moduleDestroy) 158 } 159 160 // MARK: - Exceptions 161 162 internal class ModuleNotFoundException: GenericException<String> { 163 override var reason: String { 164 "Module '\(param)' not found" 165 } 166 } 167 168 internal class FunctionNotFoundException: GenericException<(functionName: String, moduleName: String)> { 169 override var reason: String { 170 "Function '\(param.functionName)' not found in module '\(param.moduleName)'" 171 } 172 } 173 } 174