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