1 // Copyright 2022-present 650 Industries. All rights reserved.
2 
3 /**
4  Type of the IDs of shared objects.
5  */
6 public typealias SharedObjectId = Int
7 
8 /**
9  A tuple containing a pair of matching native and JS objects.
10  */
11 internal typealias SharedObjectPair = (native: SharedObject, javaScript: JavaScriptWeakObject)
12 
13 /**
14  Property name of the JS object where the shared object ID is stored.
15  */
16 let sharedObjectIdPropertyName = "__expo_shared_object_id__"
17 
18 /**
19  The registry of all shared objects used in the entire app.
20  It's been made static for simplicity.
21  */
22 public final class SharedObjectRegistry {
23   /**
24    The counter of IDs to assign to the shared object pairs.
25    The next pair added to the registry will be saved using this ID.
26    */
27   internal static var nextId: SharedObjectId = 1
28 
29   /**
30    A dictionary of shared object pairs.
31    */
32   internal static var pairs = [SharedObjectId: SharedObjectPair]()
33 
34   /**
35    A number of all pairs stored in the registry.
36    */
37   internal static var size: Int {
38     return pairs.count
39   }
40 
41   /**
42    Returns the next shared object ID and increases the counter.
43    */
44   @discardableResult
45   internal static func pullNextId() -> SharedObjectId {
46     let id = nextId
47     nextId += 1
48     return id
49   }
50 
51   /**
52    Returns a pair of shared objects with given ID or `nil` when there is no such pair in the registry.
53    */
54   internal static func get(_ id: SharedObjectId) -> SharedObjectPair? {
55     return pairs[id]
56   }
57 
58   /**
59    Adds a pair of native and JS shared object to the registry. Assigns a new shared object ID to these objects.
60    */
61   @discardableResult
62   internal static func add(native nativeObject: SharedObject, javaScript jsObject: JavaScriptObject) -> SharedObjectId {
63     let id = pullNextId()
64 
65     // Assigns the ID to the objects.
66     nativeObject.sharedObjectId = id
67     jsObject.defineProperty(sharedObjectIdPropertyName, value: id, options: [.writable])
68 
69     // Set the deallocator on the JS object that deletes the entire pair.
70     jsObject.setObjectDeallocator { delete(id) }
71 
72     // Save the pair in the dictionary.
73     pairs[id] = (native: nativeObject, javaScript: jsObject.createWeak())
74 
75     return id
76   }
77 
78   /**
79    Deletes the shared objects pair with a given ID.
80    */
81   internal static func delete(_ id: SharedObjectId) {
82     if let pair = pairs[id] {
83       // Reset an ID on the objects.
84       pair.native.sharedObjectId = 0
85       pair.javaScript.lock()?.defineProperty(sharedObjectIdPropertyName, value: 0, options: [.writable])
86 
87       // Delete the pair from the dictionary.
88       pairs[id] = nil
89     }
90   }
91 
92   /**
93    Gets the native shared object that is paired with a given JS object.
94    */
95   internal static func toNativeObject(_ jsObject: JavaScriptObject) -> SharedObject? {
96     if let objectId = try? jsObject.getProperty(sharedObjectIdPropertyName).asInt() {
97       return pairs[objectId]?.native
98     }
99     return nil
100   }
101 
102   /**
103    Gets the JS shared object that is paired with a given native object.
104    */
105   internal static func toJavaScriptObject(_ nativeObject: SharedObject) -> JavaScriptObject? {
106     let objectId = nativeObject.sharedObjectId
107     return pairs[objectId]?.javaScript.lock()
108   }
109 
110   /**
111    Creates a plain JS object and pairs it with a given native object.
112    */
113   internal static func createSharedJavaScriptObject(runtime: JavaScriptRuntime, nativeObject: SharedObject) -> JavaScriptObject {
114     let object = runtime.createObject()
115     add(native: nativeObject, javaScript: object)
116     return object
117   }
118 
119   /**
120    Ensures that there is a JS object paired with a given native object. If not, a plain JS object is created.
121    */
122   internal static func ensureSharedJavaScriptObject(runtime: JavaScriptRuntime, nativeObject: SharedObject) -> JavaScriptObject {
123     if let jsObject = toJavaScriptObject(nativeObject) {
124       // JS object for this native object already exists in the registry, just return it.
125       return jsObject
126     }
127     return createSharedJavaScriptObject(runtime: runtime, nativeObject: nativeObject)
128   }
129 
130   internal static func clear() {
131     pairs.removeAll()
132   }
133 }
134