@tileflow/react-native 0.0.0-bootstrap.0 → 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +216 -4
  2. package/TileflowNativeAdmission.podspec +34 -0
  3. package/android/build.gradle +43 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrap.kt +144 -0
  6. package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrapOkHttpNetwork.kt +89 -0
  7. package/android/src/main/java/dev/tileflow/reactnative/AdmissionCatalog.kt +137 -0
  8. package/android/src/main/java/dev/tileflow/reactnative/AdmissionDocumentScope.kt +22 -0
  9. package/android/src/main/java/dev/tileflow/reactnative/AdmissionEngine.kt +335 -0
  10. package/android/src/main/java/dev/tileflow/reactnative/AdmissionModuleProvider.kt +68 -0
  11. package/android/src/main/java/dev/tileflow/reactnative/AdmissionOkHttpNetwork.kt +86 -0
  12. package/android/src/main/java/dev/tileflow/reactnative/AdmissionProviderLease.kt +56 -0
  13. package/android/src/main/java/dev/tileflow/reactnative/AdmissionTypes.kt +85 -0
  14. package/android/src/main/java/dev/tileflow/reactnative/AdmissionUrl.kt +100 -0
  15. package/android/src/main/java/dev/tileflow/reactnative/HandlerAdmissionScheduler.kt +16 -0
  16. package/android/src/main/java/dev/tileflow/reactnative/MobileConfiguration.kt +95 -0
  17. package/android/src/main/java/dev/tileflow/reactnative/MobileConfigurationResources.kt +43 -0
  18. package/android/src/main/java/dev/tileflow/reactnative/NativeDocumentOkHttpNetwork.kt +101 -0
  19. package/android/src/main/java/dev/tileflow/reactnative/NativeDocumentRegistry.kt +136 -0
  20. package/android/src/main/java/dev/tileflow/reactnative/NativeSurfaceState.kt +115 -0
  21. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionModule.kt +234 -0
  22. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionPackage.kt +16 -0
  23. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeConfigurationModule.kt +43 -0
  24. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeDocumentsModule.kt +80 -0
  25. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeSurfaceModule.kt +277 -0
  26. package/android/src/main/res/raw/tileflow_configuration_keep.xml +3 -0
  27. package/dist/contract-DWs-lDPo.d.ts +144 -0
  28. package/dist/index.d.ts +13 -0
  29. package/dist/index.js +6989 -0
  30. package/dist/internal/camera.d.ts +65 -0
  31. package/dist/internal/camera.js +325 -0
  32. package/dist/internal/hosted-binding.d.ts +18 -0
  33. package/dist/internal/hosted-binding.js +277 -0
  34. package/dist/internal/initial-view.d.ts +11 -0
  35. package/dist/internal/initial-view.js +8 -0
  36. package/dist/internal/interactions.d.ts +76 -0
  37. package/dist/internal/interactions.js +794 -0
  38. package/dist/internal/native-admission-bridge.d.ts +29 -0
  39. package/dist/internal/native-admission-bridge.js +351 -0
  40. package/dist/internal/native-admission.d.ts +78 -0
  41. package/dist/internal/native-admission.js +1614 -0
  42. package/dist/internal/native-appearance.d.ts +21 -0
  43. package/dist/internal/native-appearance.js +110 -0
  44. package/dist/internal/native-configuration-bridge.d.ts +9 -0
  45. package/dist/internal/native-configuration-bridge.js +343 -0
  46. package/dist/internal/session.d.ts +125 -0
  47. package/dist/internal/session.js +887 -0
  48. package/dist/internal/source-state.d.ts +11 -0
  49. package/dist/internal/source-state.js +26 -0
  50. package/dist/native-admission-contract-k7LKE1vW.d.ts +89 -0
  51. package/docs/native-admission.md +108 -0
  52. package/docs/native-configuration.md +210 -0
  53. package/docs/native-interactions.md +161 -0
  54. package/docs/native-location.md +148 -0
  55. package/ios/TFAdmissionBootstrap.h +17 -0
  56. package/ios/TFAdmissionBootstrap.mm +168 -0
  57. package/ios/TFAdmissionCatalog.h +13 -0
  58. package/ios/TFAdmissionCatalog.mm +193 -0
  59. package/ios/TFAdmissionDocumentScope.h +7 -0
  60. package/ios/TFAdmissionDocumentScope.mm +45 -0
  61. package/ios/TFAdmissionEngine.h +57 -0
  62. package/ios/TFAdmissionEngine.mm +371 -0
  63. package/ios/TFAdmissionInstallation.h +27 -0
  64. package/ios/TFAdmissionInstallation.mm +117 -0
  65. package/ios/TFAdmissionNetwork.h +16 -0
  66. package/ios/TFAdmissionNetwork.mm +268 -0
  67. package/ios/TFAdmissionURL.mm +116 -0
  68. package/ios/TFDocumentBounds.h +9 -0
  69. package/ios/TFDocumentBounds.mm +19 -0
  70. package/ios/TFMobileConfiguration.h +23 -0
  71. package/ios/TFMobileConfiguration.mm +139 -0
  72. package/ios/TFNativeAdmissionURLProtocol.mm +69 -0
  73. package/ios/TFNativeDocumentRegistry.h +28 -0
  74. package/ios/TFNativeDocumentRegistry.mm +193 -0
  75. package/ios/TFNativeDocumentURL.h +6 -0
  76. package/ios/TFNativeSurfaceState.h +28 -0
  77. package/ios/TFNativeSurfaceState.mm +128 -0
  78. package/ios/TileflowNativeAdmission.h +9 -0
  79. package/ios/TileflowNativeAdmission.mm +184 -0
  80. package/ios/TileflowNativeConfiguration.mm +41 -0
  81. package/ios/TileflowNativeDocuments.h +12 -0
  82. package/ios/TileflowNativeDocuments.mm +105 -0
  83. package/ios/TileflowNativeSurface.mm +403 -0
  84. package/ios/tileflow_native_admission.rb +45 -0
  85. package/package.json +51 -7
  86. package/react-native.config.cjs +13 -0
@@ -0,0 +1,115 @@
1
+ package dev.tileflow.reactnative
2
+
3
+ // All methods run on the view's UI queue. Identity is captured before each native frame.
4
+ internal class NativeSurfaceState(private val surface: String, private val emit: (Map<String, Any>) -> Unit) {
5
+ private var token = ""
6
+ private var style: Any? = null
7
+ private var frame: Any? = null
8
+ private var frameLayout = 0L
9
+ private var visible = false
10
+ private var closed = false
11
+ private var failed = false
12
+ private var layoutEpoch = 1L
13
+ private var committed = 0L
14
+ private var requested = 0L
15
+ private var reported = false
16
+ private var sequence = 0L
17
+ private var command = 0L
18
+ private var gestureSequence = 0L
19
+ private var gesture = 0L
20
+ private val pending = ArrayDeque<Map<String, Any>>()
21
+ var awaitingSequence: Long? = null
22
+ private set
23
+ val active: Boolean get() = !closed && !failed
24
+ val pendingCount: Int get() = pending.size
25
+
26
+ fun expect(value: String) {
27
+ check(!closed && AdmissionUrl.validToken(value)) { "Native surface operation failed." }
28
+ token = value; style = null; frame = null; committed = 0; requested = 0; reported = false; failed = false; gesture = 0
29
+ }
30
+ fun layout(isVisible: Boolean) {
31
+ if (closed) return
32
+ visible = isVisible
33
+ if (layoutEpoch >= 9007199254740991L) { fail(); return }
34
+ layoutEpoch++; committed = 0; requested = 0; frame = null; reported = false; gesture = 0
35
+ enqueue("invalidate")
36
+ }
37
+ fun loaded(value: String, identity: Any) {
38
+ if (!active || value != token || style === identity) return
39
+ style = identity; requested = 0; frame = null; reported = false
40
+ enqueue("style")
41
+ }
42
+ fun commit(value: String): Long {
43
+ check(active && visible && value == token && style != null) { "Native surface operation failed." }
44
+ committed = layoutEpoch; requested = 0; frame = null; reported = false
45
+ return committed
46
+ }
47
+ fun request(value: String) {
48
+ check(active && visible && value == token && style != null && committed == layoutEpoch && !reported) { "Native surface operation failed." }
49
+ requested = layoutEpoch; frame = null
50
+ }
51
+ fun frameStart(identity: Any?) {
52
+ frame = if (active && visible && identity === style && committed == layoutEpoch && requested == layoutEpoch) identity else null
53
+ frameLayout = layoutEpoch
54
+ }
55
+ fun frameEnd(identity: Any?, fully: Boolean) {
56
+ val captured = frame; frame = null
57
+ if (!active || reported || !visible || !fully || captured == null ||
58
+ captured !== identity || captured !== style || frameLayout != layoutEpoch || committed != layoutEpoch || requested != layoutEpoch) return
59
+ reported = true; requested = 0
60
+ enqueue("render")
61
+ }
62
+ fun beginCommand(value: Long): Long {
63
+ if (!active || !visible || style == null || gesture != 0L || value <= command || value > 9007199254740991L) return 0
64
+ command = value; committed = 0; requested = 0; frame = null; reported = false
65
+ return enqueue("invalidate")
66
+ }
67
+ fun cancelCommand(value: Long) { if (value > command && value <= 9007199254740991L) command = value }
68
+ fun gestureStart(view: Map<String, Any>) {
69
+ if (!active || !visible || style == null || gesture != 0L) return
70
+ if (gestureSequence >= 9007199254740991L) { fail(); return }
71
+ requested = 0; frame = null; gesture = ++gestureSequence
72
+ enqueue("gesture-start", mapOf("gesture" to gesture, "view" to view))
73
+ }
74
+ fun gestureChange(view: Map<String, Any>) {
75
+ if (active && gesture != 0L) enqueue("gesture-change", mapOf("gesture" to gesture, "view" to view))
76
+ }
77
+ fun gestureEnd(view: Map<String, Any>) {
78
+ if (!active || gesture == 0L) return
79
+ val completed = gesture; gesture = 0
80
+ enqueue("gesture-end", mapOf("gesture" to completed, "view" to view))
81
+ }
82
+ fun fail() {
83
+ if (closed || failed) return
84
+ failed = true; committed = 0; requested = 0; frame = null; gesture = 0
85
+ enqueue("error")
86
+ }
87
+ private fun enqueue(kindInput: String, fieldsInput: Map<String, Any> = emptyMap()): Long {
88
+ if (closed || token.isEmpty() || sequence >= 9007199254740991L) return 0
89
+ var kind = kindInput
90
+ var fields = fieldsInput
91
+ val last = pending.lastOrNull()
92
+ if ((kind == "gesture-change" || kind == "invalidate") && last?.get("kind") == kind && last["style"] == token && last["gesture"] == fields["gesture"]) pending.removeLast()
93
+ val overflow = pending.size >= 32
94
+ if (overflow) {
95
+ pending.clear(); failed = true; frame = null; committed = 0; requested = 0; gesture = 0
96
+ kind = "error"; fields = emptyMap()
97
+ }
98
+ val emitted = ++sequence
99
+ pending.addLast(mapOf("surface" to surface, "style" to token, "layout" to layoutEpoch, "sequence" to emitted, "kind" to kind) + fields)
100
+ drain()
101
+ return if (overflow) 0 else emitted
102
+ }
103
+ private fun drain() {
104
+ if (closed || awaitingSequence != null || pending.isEmpty()) return
105
+ val event = pending.removeFirst()
106
+ awaitingSequence = event["sequence"] as Long
107
+ try { emit(event) } catch (_: Exception) { close() }
108
+ }
109
+ fun acknowledge(value: Long) {
110
+ if (awaitingSequence != value) return
111
+ awaitingSequence = null; drain()
112
+ }
113
+ fun close() { closed = true; style = null; frame = null; committed = 0; requested = 0; gesture = 0; pending.clear(); awaitingSequence = null }
114
+ override fun toString() = "NativeSurfaceState"
115
+ }
@@ -0,0 +1,234 @@
1
+ package dev.tileflow.reactnative
2
+
3
+ import android.util.Base64
4
+ import java.util.UUID
5
+ import com.facebook.react.bridge.Arguments
6
+ import com.facebook.react.bridge.LifecycleEventListener
7
+ import com.facebook.react.bridge.Promise
8
+ import com.facebook.react.bridge.ReactApplicationContext
9
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
10
+ import com.facebook.react.bridge.ReactMethod
11
+ import com.facebook.react.bridge.ReadableArray
12
+ import com.facebook.react.bridge.ReadableMap
13
+ import com.facebook.react.modules.core.DeviceEventManagerModule
14
+ import com.facebook.react.common.LifecycleState
15
+ import org.maplibre.android.MapLibre
16
+
17
+ class TileflowNativeAdmissionModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(context), LifecycleEventListener {
18
+ private val scheduler = HandlerAdmissionScheduler()
19
+ private var installation: String? = null
20
+ private var lease: AdmissionProviderLease? = null
21
+ private var engine: AdmissionEngine? = null
22
+ private var network: AdmissionOkHttpNetwork? = null
23
+ private var bootstrapRequests: AdmissionBootstrap? = null
24
+ private var bootstrapNetwork: AdmissionBootstrapOkHttpNetwork? = null
25
+ private var listeners = 0
26
+ private var invalidated = false
27
+ private var lastRemoved: String? = null
28
+ private var lastRemoval: Map<String, Boolean>? = null
29
+
30
+ init { context.addLifecycleEventListener(this) }
31
+ override fun getName() = "TileflowNativeAdmission"
32
+
33
+ @ReactMethod fun addListener(eventName: String) { scheduler.dispatch { if (eventName == "TileflowNativeAdmissionEvent") listeners++ } }
34
+ @ReactMethod fun removeListeners(count: Double) { scheduler.dispatch { listeners = maxOf(0, listeners - count.toInt()) } }
35
+
36
+ @ReactMethod fun install(promise: Promise) = action(promise) {
37
+ if (invalidated || installation != null || listeners <= 0) invalid()
38
+ val ownership = AdmissionProviderLease({ MapLibre.getModuleProvider() }, { MapLibre.setModuleProvider(it) })
39
+ if (ownership.previous is AdmissionModuleProvider) invalid()
40
+ val id = UUID.randomUUID().toString()
41
+ lease = ownership
42
+ installation = id
43
+ try {
44
+ val transport = AdmissionOkHttpNetwork()
45
+ network = transport
46
+ val admission = AdmissionEngine(id, scheduler, transport, ownership::owns, ::emit)
47
+ engine = admission
48
+ val bootstrapTransport = AdmissionBootstrapOkHttpNetwork()
49
+ bootstrapNetwork = bootstrapTransport
50
+ bootstrapRequests = AdmissionBootstrap(id, scheduler, bootstrapTransport) { admission.checkOwnership() }
51
+ ownership.install(AdmissionModuleProvider(ownership.previous, admission))
52
+ admission.lifecycle(reactApplicationContext.lifecycleState == LifecycleState.RESUMED)
53
+ if (!admission.checkOwnership()) invalid()
54
+ Arguments.makeNativeMap(mapOf("installation" to id))
55
+ } catch (_: Exception) {
56
+ // Includes failures after writing the provider or emitting lifecycle.
57
+ // Keep failed cleanup state available for a later teardown attempt.
58
+ try { shutdown() } catch (_: Exception) { /* No successful acknowledgement. */ }
59
+ invalid()
60
+ }
61
+ }
62
+
63
+ @ReactMethod fun registerContext(id: String, registration: ReadableMap, promise: Promise) = action(promise) {
64
+ val admission = current(id)
65
+ val mapId = if (registration.isNull("mapId")) null else registration.getString("mapId") ?: invalid()
66
+ val context = admission.register(mapId, readResources(registration.getArray("resources") ?: invalid()))
67
+ try { (bootstrapRequests ?: invalid()).register(context, mapId) }
68
+ catch (error: Exception) { admission.retire(context); throw error }
69
+ Arguments.makeNativeMap(mapOf("context" to context, "generation" to 1))
70
+ }
71
+
72
+ @ReactMethod fun extendContext(id: String, context: String, source: ReadableArray, promise: Promise) = action(promise) {
73
+ if (!AdmissionUrl.validToken(context)) invalid()
74
+ Arguments.makeNativeMap(mapOf("resources" to current(id).extend(context, readResources(source))))
75
+ }
76
+
77
+ private fun readResources(source: ReadableArray): List<AdmissionResource> {
78
+ if (source.size() > AdmissionLimits.RESOURCES) invalid()
79
+ var characters = 0
80
+ return (0 until source.size()).map { index ->
81
+ val item = source.getMap(index) ?: invalid()
82
+ val keys = item.keySetIterator()
83
+ var fields = 0
84
+ while (keys.hasNextKey()) if (++fields > 5 || keys.nextKey() !in setOf("url", "scope", "tilesetId", "template", "fontStacks")) invalid()
85
+ val url = item.getString("url") ?: invalid()
86
+ val scope = item.getString("scope") ?: invalid()
87
+ val tileset = if (item.hasKey("tilesetId") && !item.isNull("tilesetId")) item.getString("tilesetId") else null
88
+ val template = if (item.hasKey("template") && !item.isNull("template")) item.getString("template") else null
89
+ val stacks = if (item.hasKey("fontStacks")) {
90
+ val array = item.getArray("fontStacks") ?: invalid()
91
+ if (array.size() > 16) invalid()
92
+ (0 until array.size()).map { position ->
93
+ val value = array.getString(position) ?: invalid()
94
+ if (value.length > 256) invalid()
95
+ characters += value.length * 4
96
+ value
97
+ }
98
+ } else null
99
+ characters += url.length + scope.length + (tileset?.length ?: 0) + (template?.length ?: 0) + 128
100
+ if (characters > AdmissionLimits.BRIDGE_BYTES || url.length > AdmissionLimits.URL) invalid()
101
+ AdmissionResource(url, scope, tileset, template, stacks)
102
+ }
103
+ }
104
+
105
+ @ReactMethod fun retireContext(id: String, context: String, promise: Promise) = action(promise) {
106
+ if (!AdmissionUrl.validToken(context)) invalid()
107
+ if (installation == id) {
108
+ bootstrapRequests?.retire(context)
109
+ engine?.retire(context)
110
+ } else if (lastRemoved != id) invalid()
111
+ Arguments.makeNativeMap(mapOf("retired" to true))
112
+ }
113
+
114
+ internal fun documentScope(id: String, context: String, maximumBytes: Int = 8388608): NativeDocumentScope {
115
+ if (!AdmissionUrl.validToken(context)) invalid()
116
+ return createAdmissionDocumentScope(current(id), context, maximumBytes)
117
+ }
118
+
119
+ @ReactMethod fun bootstrap(id: String, context: String, request: String, url: String, credential: String, body: String, promise: Promise) {
120
+ scheduler.dispatch {
121
+ try {
122
+ current(id)
123
+ (bootstrapRequests ?: invalid()).start(context, request, url, credential, body) { reply ->
124
+ if (reply == null) reject(promise)
125
+ else promise.resolve(Arguments.makeNativeMap(mapOf(
126
+ "status" to reply.status, "cacheControl" to reply.cacheControl,
127
+ "bodyBase64" to Base64.encodeToString(reply.body, Base64.NO_WRAP))))
128
+ }
129
+ } catch (_: Exception) { reject(promise) }
130
+ }
131
+ }
132
+
133
+ @ReactMethod fun cancelBootstrap(id: String, context: String, request: String, promise: Promise) = action(promise) {
134
+ if (!AdmissionUrl.validToken(context) || !AdmissionUrl.validToken(request)) invalid()
135
+ if (installation == id) (bootstrapRequests ?: invalid()).cancel(context, request)
136
+ else if (lastRemoved != id) invalid()
137
+ Arguments.makeNativeMap(mapOf("cancelled" to true))
138
+ }
139
+
140
+ @ReactMethod fun completeBatch(id: String, context: String, generation: Double, batch: String, source: ReadableArray, promise: Promise) = action(promise) {
141
+ val admission = current(id)
142
+ if (!AdmissionUrl.validToken(context) || !AdmissionUrl.validToken(batch) || generation != 1.0 || source.size() > AdmissionLimits.BATCH) invalid()
143
+ var characters = 0
144
+ fun string(map: ReadableMap, key: String, maximum: Int = AdmissionLimits.URL): String {
145
+ val value = map.getString(key) ?: invalid()
146
+ characters += value.length
147
+ if (value.length > maximum || characters > AdmissionLimits.BRIDGE_BYTES) invalid()
148
+ return value
149
+ }
150
+ fun strings(map: ReadableMap, key: String, maximum: Int): List<String> {
151
+ val array = map.getArray(key) ?: invalid()
152
+ if (array.size() > maximum) invalid()
153
+ return (0 until array.size()).map { index ->
154
+ val value = array.getString(index) ?: invalid()
155
+ characters += value.length
156
+ if (value.length > AdmissionLimits.URL || characters > AdmissionLimits.BRIDGE_BYTES) invalid()
157
+ value
158
+ }
159
+ }
160
+ val results = (0 until source.size()).map { index ->
161
+ val item = source.getMap(index) ?: invalid()
162
+ val ticket = string(item, "ticket", 96)
163
+ if (!AdmissionUrl.validToken(ticket)) invalid()
164
+ when (string(item, "kind", 16)) {
165
+ "delegate" -> AdmissionResult.Delegate(ticket)
166
+ "reject" -> AdmissionResult.Reject(ticket)
167
+ "grant" -> {
168
+ val budget = item.getDouble("validForMs")
169
+ if (!budget.isFinite() || budget != budget.toLong().toDouble() || budget <= 0 || budget > AdmissionLimits.MAX_VALID_MS) invalid()
170
+ val authority = item.getMap("authority") ?: invalid()
171
+ AdmissionResult.Grant(ticket, budget.toLong(), AdmissionAuthority(string(authority, "grant", AdmissionLimits.GRANT),
172
+ string(authority, "mapId", 64), strings(authority, "resourceOrigins", 2), strings(authority, "resourceScopes", 6), strings(authority, "tilesetIds", 18)))
173
+ }
174
+ else -> invalid()
175
+ }
176
+ }
177
+ Arguments.makeNativeMap(mapOf("accepted" to admission.complete(context, 1, batch, results)))
178
+ }
179
+
180
+ @ReactMethod fun remove(id: String, promise: Promise) = action(promise) {
181
+ if (installation == null && id == lastRemoved) return@action Arguments.makeNativeMap(lastRemoval ?: invalid())
182
+ if (id != installation) invalid()
183
+ Arguments.makeNativeMap(shutdown())
184
+ }
185
+
186
+ private fun current(id: String): AdmissionEngine {
187
+ if (installation != id || invalidated) invalid()
188
+ val admission = engine ?: invalid()
189
+ if (!admission.checkOwnership()) invalid()
190
+ return admission
191
+ }
192
+ private fun action(promise: Promise, block: () -> Any) {
193
+ scheduler.dispatch {
194
+ try { promise.resolve(block()) }
195
+ catch (_: Exception) { reject(promise) }
196
+ }
197
+ }
198
+ private fun reject(promise: Promise) { promise.reject("NATIVE_ADMISSION_UNAVAILABLE", "Native resource admission failed") }
199
+ private fun emit(event: Map<String, Any>) {
200
+ when (event["kind"]) {
201
+ "retired" -> (event["context"] as? String)?.let {
202
+ bootstrapRequests?.retire(it)
203
+ reactApplicationContext.getNativeModule(TileflowNativeDocumentsModule::class.java)?.retireNativeContext(it)
204
+ }
205
+ "ownershipLost" -> bootstrapRequests?.close()
206
+ "lifecycle" -> bootstrapRequests?.lifecycle(event["foreground"] == true)
207
+ }
208
+ if (listeners <= 0 || invalidated) throw IllegalStateException("Native admission listener is unavailable")
209
+ reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
210
+ .emit("TileflowNativeAdmissionEvent", Arguments.makeNativeMap(event))
211
+ }
212
+
213
+ override fun onHostPause() { scheduler.dispatch { engine?.lifecycle(false) } }
214
+ override fun onHostResume() { scheduler.dispatch { engine?.lifecycle(true) } }
215
+ override fun onHostDestroy() { scheduler.dispatch { invalidated = true; teardown() } }
216
+ override fun invalidate() {
217
+ reactApplicationContext.removeLifecycleEventListener(this)
218
+ scheduler.dispatch { invalidated = true; teardown() }
219
+ super.invalidate()
220
+ }
221
+ private fun teardown() {
222
+ try { shutdown() } catch (_: Exception) { /* Retain failed cleanup for retry; never report success. */ }
223
+ }
224
+ private fun shutdown(): Map<String, Boolean> {
225
+ val id = installation ?: return lastRemoval ?: mapOf("removed" to false, "ownershipLost" to false)
226
+ bootstrapRequests?.close(); engine?.close(); network?.close(); bootstrapNetwork?.close()
227
+ val ack = (lease ?: invalid()).remove()
228
+ lastRemoved = id; lastRemoval = ack
229
+ installation = null; engine = null; network = null; lease = null
230
+ bootstrapRequests = null; bootstrapNetwork = null
231
+ return ack
232
+ }
233
+ private fun invalid(): Nothing = throw IllegalArgumentException("Invalid native admission")
234
+ }
@@ -0,0 +1,16 @@
1
+ package dev.tileflow.reactnative
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class TileflowNativeAdmissionPackage : ReactPackage {
9
+ override fun createNativeModules(context: ReactApplicationContext): List<NativeModule> = listOf(
10
+ TileflowNativeAdmissionModule(context),
11
+ TileflowNativeConfigurationModule(context),
12
+ TileflowNativeDocumentsModule(context),
13
+ TileflowNativeSurfaceModule(context)
14
+ )
15
+ override fun createViewManagers(context: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()
16
+ }
@@ -0,0 +1,43 @@
1
+ package dev.tileflow.reactnative
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.Promise
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
7
+ import com.facebook.react.bridge.ReactMethod
8
+
9
+ class TileflowNativeConfigurationModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(context) {
10
+ private var invalidated = false
11
+
12
+ override fun getName() = "TileflowNativeConfiguration"
13
+
14
+ @ReactMethod fun readConfiguration(promise: Promise) {
15
+ synchronized(this) {
16
+ if (invalidated) {
17
+ promise.reject("NATIVE_CONFIGURATION_UNAVAILABLE", "Native application configuration is unavailable.")
18
+ return
19
+ }
20
+ val configuration = try {
21
+ applicationConfiguration.read { readMobileConfigurationResources(reactApplicationContext) }
22
+ } catch (_: Exception) {
23
+ promise.reject("NATIVE_CONFIGURATION_INVALID", "Native application configuration is invalid.")
24
+ return
25
+ }
26
+ // No constants, events or public state. This bounded response belongs to the private bridge.
27
+ promise.resolve(Arguments.makeNativeMap(mapOf(
28
+ "apiOrigin" to configuration.apiOrigin,
29
+ "credential" to configuration.credential
30
+ )))
31
+ }
32
+ }
33
+
34
+ override fun invalidate() {
35
+ synchronized(this) { invalidated = true }
36
+ super.invalidate()
37
+ }
38
+
39
+ companion object {
40
+ // The cache retains immutable application data, never a React context or Map lifetime.
41
+ private val applicationConfiguration = MobileConfigurationCache()
42
+ }
43
+ }
@@ -0,0 +1,80 @@
1
+ package dev.tileflow.reactnative
2
+
3
+ import android.util.Base64
4
+ import com.facebook.react.bridge.Arguments
5
+ import com.facebook.react.bridge.LifecycleEventListener
6
+ import com.facebook.react.bridge.Promise
7
+ import com.facebook.react.bridge.ReactApplicationContext
8
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
9
+ import com.facebook.react.bridge.ReactMethod
10
+ import com.facebook.react.common.LifecycleState
11
+
12
+ class TileflowNativeDocumentsModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(context), LifecycleEventListener {
13
+ private val scheduler = HandlerAdmissionScheduler()
14
+ private val network = NativeDocumentOkHttpNetwork()
15
+ private val registry = NativeDocumentRegistry(scheduler, { url, limit, scope, developmentOrigin, active, completion ->
16
+ if (scope == null) network.start(url, limit, active, completion, developmentOrigin)
17
+ else scope.load(url, completion)
18
+ })
19
+ @Volatile private var invalidated = false
20
+
21
+ init {
22
+ registry.lifecycle(context.lifecycleState == LifecycleState.RESUMED)
23
+ context.addLifecycleEventListener(this)
24
+ }
25
+ override fun getName() = "TileflowNativeDocuments"
26
+
27
+ @ReactMethod fun openDocument(url: String, maximumBytes: Double, installation: String?, context: String?, developmentOrigin: String?, promise: Promise) = action(promise) {
28
+ if (invalidated || !maximumBytes.isFinite() || maximumBytes != maximumBytes.toInt().toDouble() || maximumBytes < 1 || maximumBytes > 8388608 ||
29
+ (installation == null) != (context == null)) invalid()
30
+ val scope = if (installation == null) null else {
31
+ if (!AdmissionUrl.validToken(installation) || !AdmissionUrl.validToken(context!!)) invalid()
32
+ val admission = reactApplicationContext.getNativeModule(TileflowNativeAdmissionModule::class.java) ?: invalid()
33
+ admission.documentScope(installation!!, context!!, maximumBytes.toInt())
34
+ }
35
+ Arguments.makeNativeMap(mapOf("document" to registry.open(url, maximumBytes.toInt(), scope, developmentOrigin)))
36
+ }
37
+
38
+ @ReactMethod fun documentResponse(document: String, promise: Promise) {
39
+ scheduler.dispatch {
40
+ try {
41
+ if (invalidated || !AdmissionUrl.validToken(document)) invalid()
42
+ registry.response(document) { response ->
43
+ if (response == null || invalidated) reject(promise)
44
+ else promise.resolve(Arguments.makeNativeMap(mapOf("url" to response.url, "status" to response.status)))
45
+ }
46
+ } catch (_: Exception) { reject(promise) }
47
+ }
48
+ }
49
+
50
+ @ReactMethod fun documentChunk(document: String, maximumBytes: Double, promise: Promise) = action(promise) {
51
+ if (invalidated || !maximumBytes.isFinite() || maximumBytes != maximumBytes.toInt().toDouble() || maximumBytes < 1 || maximumBytes > 65536) invalid()
52
+ val chunk = registry.chunk(document, maximumBytes.toInt())
53
+ val encoded = Base64.encodeToString(chunk.bytes, Base64.NO_WRAP)
54
+ chunk.bytes.fill(0)
55
+ Arguments.makeNativeMap(mapOf("bodyBase64" to encoded, "last" to chunk.last))
56
+ }
57
+
58
+ @ReactMethod fun cancelDocument(document: String, promise: Promise) = action(promise) {
59
+ registry.cancel(document)
60
+ Arguments.makeNativeMap(mapOf("cancelled" to true))
61
+ }
62
+
63
+ internal fun retireNativeContext(context: String) { registry.retireContext(context) }
64
+ private fun action(promise: Promise, block: () -> Any) {
65
+ scheduler.dispatch {
66
+ try { promise.resolve(block()) }
67
+ catch (_: Exception) { reject(promise) }
68
+ }
69
+ }
70
+ private fun reject(promise: Promise) { promise.reject("NATIVE_DOCUMENT_UNAVAILABLE", "Native document acquisition failed.") }
71
+ private fun invalid(): Nothing = throw IllegalArgumentException("Native document acquisition failed.")
72
+ override fun onHostPause() { scheduler.dispatch { registry.lifecycle(false) } }
73
+ override fun onHostResume() { scheduler.dispatch { if (!invalidated) registry.lifecycle(true) } }
74
+ override fun onHostDestroy() { scheduler.dispatch { invalidated = true; registry.close(); network.close() } }
75
+ override fun invalidate() {
76
+ reactApplicationContext.removeLifecycleEventListener(this)
77
+ scheduler.dispatch { invalidated = true; registry.close(); network.close() }
78
+ super.invalidate()
79
+ }
80
+ }