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

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 +236 -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 +82 -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,236 @@
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 com.facebook.react.module.annotations.ReactModule
16
+ import org.maplibre.android.MapLibre
17
+
18
+ @ReactModule(name = "TileflowNativeAdmission")
19
+ class TileflowNativeAdmissionModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(context), LifecycleEventListener {
20
+ private val scheduler = HandlerAdmissionScheduler()
21
+ private var installation: String? = null
22
+ private var lease: AdmissionProviderLease? = null
23
+ private var engine: AdmissionEngine? = null
24
+ private var network: AdmissionOkHttpNetwork? = null
25
+ private var bootstrapRequests: AdmissionBootstrap? = null
26
+ private var bootstrapNetwork: AdmissionBootstrapOkHttpNetwork? = null
27
+ private var listeners = 0
28
+ private var invalidated = false
29
+ private var lastRemoved: String? = null
30
+ private var lastRemoval: Map<String, Boolean>? = null
31
+
32
+ init { context.addLifecycleEventListener(this) }
33
+ override fun getName() = "TileflowNativeAdmission"
34
+
35
+ @ReactMethod fun addListener(eventName: String) { scheduler.dispatch { if (eventName == "TileflowNativeAdmissionEvent") listeners++ } }
36
+ @ReactMethod fun removeListeners(count: Double) { scheduler.dispatch { listeners = maxOf(0, listeners - count.toInt()) } }
37
+
38
+ @ReactMethod fun install(promise: Promise) = action(promise) {
39
+ if (invalidated || installation != null || listeners <= 0) invalid()
40
+ val ownership = AdmissionProviderLease({ MapLibre.getModuleProvider() }, { MapLibre.setModuleProvider(it) })
41
+ if (ownership.previous is AdmissionModuleProvider) invalid()
42
+ val id = UUID.randomUUID().toString()
43
+ lease = ownership
44
+ installation = id
45
+ try {
46
+ val transport = AdmissionOkHttpNetwork()
47
+ network = transport
48
+ val admission = AdmissionEngine(id, scheduler, transport, ownership::owns, ::emit)
49
+ engine = admission
50
+ val bootstrapTransport = AdmissionBootstrapOkHttpNetwork()
51
+ bootstrapNetwork = bootstrapTransport
52
+ bootstrapRequests = AdmissionBootstrap(id, scheduler, bootstrapTransport) { admission.checkOwnership() }
53
+ ownership.install(AdmissionModuleProvider(ownership.previous, admission))
54
+ admission.lifecycle(reactApplicationContext.lifecycleState == LifecycleState.RESUMED)
55
+ if (!admission.checkOwnership()) invalid()
56
+ Arguments.makeNativeMap(mapOf("installation" to id))
57
+ } catch (_: Exception) {
58
+ // Includes failures after writing the provider or emitting lifecycle.
59
+ // Keep failed cleanup state available for a later teardown attempt.
60
+ try { shutdown() } catch (_: Exception) { /* No successful acknowledgement. */ }
61
+ invalid()
62
+ }
63
+ }
64
+
65
+ @ReactMethod fun registerContext(id: String, registration: ReadableMap, promise: Promise) = action(promise) {
66
+ val admission = current(id)
67
+ val mapId = if (registration.isNull("mapId")) null else registration.getString("mapId") ?: invalid()
68
+ val context = admission.register(mapId, readResources(registration.getArray("resources") ?: invalid()))
69
+ try { (bootstrapRequests ?: invalid()).register(context, mapId) }
70
+ catch (error: Exception) { admission.retire(context); throw error }
71
+ Arguments.makeNativeMap(mapOf("context" to context, "generation" to 1))
72
+ }
73
+
74
+ @ReactMethod fun extendContext(id: String, context: String, source: ReadableArray, promise: Promise) = action(promise) {
75
+ if (!AdmissionUrl.validToken(context)) invalid()
76
+ Arguments.makeNativeMap(mapOf("resources" to current(id).extend(context, readResources(source))))
77
+ }
78
+
79
+ private fun readResources(source: ReadableArray): List<AdmissionResource> {
80
+ if (source.size() > AdmissionLimits.RESOURCES) invalid()
81
+ var characters = 0
82
+ return (0 until source.size()).map { index ->
83
+ val item = source.getMap(index) ?: invalid()
84
+ val keys = item.keySetIterator()
85
+ var fields = 0
86
+ while (keys.hasNextKey()) if (++fields > 5 || keys.nextKey() !in setOf("url", "scope", "tilesetId", "template", "fontStacks")) invalid()
87
+ val url = item.getString("url") ?: invalid()
88
+ val scope = item.getString("scope") ?: invalid()
89
+ val tileset = if (item.hasKey("tilesetId") && !item.isNull("tilesetId")) item.getString("tilesetId") else null
90
+ val template = if (item.hasKey("template") && !item.isNull("template")) item.getString("template") else null
91
+ val stacks = if (item.hasKey("fontStacks")) {
92
+ val array = item.getArray("fontStacks") ?: invalid()
93
+ if (array.size() > 16) invalid()
94
+ (0 until array.size()).map { position ->
95
+ val value = array.getString(position) ?: invalid()
96
+ if (value.length > 256) invalid()
97
+ characters += value.length * 4
98
+ value
99
+ }
100
+ } else null
101
+ characters += url.length + scope.length + (tileset?.length ?: 0) + (template?.length ?: 0) + 128
102
+ if (characters > AdmissionLimits.BRIDGE_BYTES || url.length > AdmissionLimits.URL) invalid()
103
+ AdmissionResource(url, scope, tileset, template, stacks)
104
+ }
105
+ }
106
+
107
+ @ReactMethod fun retireContext(id: String, context: String, promise: Promise) = action(promise) {
108
+ if (!AdmissionUrl.validToken(context)) invalid()
109
+ if (installation == id) {
110
+ bootstrapRequests?.retire(context)
111
+ engine?.retire(context)
112
+ } else if (lastRemoved != id) invalid()
113
+ Arguments.makeNativeMap(mapOf("retired" to true))
114
+ }
115
+
116
+ internal fun documentScope(id: String, context: String, maximumBytes: Int = 8388608): NativeDocumentScope {
117
+ if (!AdmissionUrl.validToken(context)) invalid()
118
+ return createAdmissionDocumentScope(current(id), context, maximumBytes)
119
+ }
120
+
121
+ @ReactMethod fun bootstrap(id: String, context: String, request: String, url: String, credential: String, body: String, promise: Promise) {
122
+ scheduler.dispatch {
123
+ try {
124
+ current(id)
125
+ (bootstrapRequests ?: invalid()).start(context, request, url, credential, body) { reply ->
126
+ if (reply == null) reject(promise)
127
+ else promise.resolve(Arguments.makeNativeMap(mapOf(
128
+ "status" to reply.status, "cacheControl" to reply.cacheControl,
129
+ "bodyBase64" to Base64.encodeToString(reply.body, Base64.NO_WRAP))))
130
+ }
131
+ } catch (_: Exception) { reject(promise) }
132
+ }
133
+ }
134
+
135
+ @ReactMethod fun cancelBootstrap(id: String, context: String, request: String, promise: Promise) = action(promise) {
136
+ if (!AdmissionUrl.validToken(context) || !AdmissionUrl.validToken(request)) invalid()
137
+ if (installation == id) (bootstrapRequests ?: invalid()).cancel(context, request)
138
+ else if (lastRemoved != id) invalid()
139
+ Arguments.makeNativeMap(mapOf("cancelled" to true))
140
+ }
141
+
142
+ @ReactMethod fun completeBatch(id: String, context: String, generation: Double, batch: String, source: ReadableArray, promise: Promise) = action(promise) {
143
+ val admission = current(id)
144
+ if (!AdmissionUrl.validToken(context) || !AdmissionUrl.validToken(batch) || generation != 1.0 || source.size() > AdmissionLimits.BATCH) invalid()
145
+ var characters = 0
146
+ fun string(map: ReadableMap, key: String, maximum: Int = AdmissionLimits.URL): String {
147
+ val value = map.getString(key) ?: invalid()
148
+ characters += value.length
149
+ if (value.length > maximum || characters > AdmissionLimits.BRIDGE_BYTES) invalid()
150
+ return value
151
+ }
152
+ fun strings(map: ReadableMap, key: String, maximum: Int): List<String> {
153
+ val array = map.getArray(key) ?: invalid()
154
+ if (array.size() > maximum) invalid()
155
+ return (0 until array.size()).map { index ->
156
+ val value = array.getString(index) ?: invalid()
157
+ characters += value.length
158
+ if (value.length > AdmissionLimits.URL || characters > AdmissionLimits.BRIDGE_BYTES) invalid()
159
+ value
160
+ }
161
+ }
162
+ val results = (0 until source.size()).map { index ->
163
+ val item = source.getMap(index) ?: invalid()
164
+ val ticket = string(item, "ticket", 96)
165
+ if (!AdmissionUrl.validToken(ticket)) invalid()
166
+ when (string(item, "kind", 16)) {
167
+ "delegate" -> AdmissionResult.Delegate(ticket)
168
+ "reject" -> AdmissionResult.Reject(ticket)
169
+ "grant" -> {
170
+ val budget = item.getDouble("validForMs")
171
+ if (!budget.isFinite() || budget != budget.toLong().toDouble() || budget <= 0 || budget > AdmissionLimits.MAX_VALID_MS) invalid()
172
+ val authority = item.getMap("authority") ?: invalid()
173
+ AdmissionResult.Grant(ticket, budget.toLong(), AdmissionAuthority(string(authority, "grant", AdmissionLimits.GRANT),
174
+ string(authority, "mapId", 64), strings(authority, "resourceOrigins", 2), strings(authority, "resourceScopes", 6), strings(authority, "tilesetIds", 18)))
175
+ }
176
+ else -> invalid()
177
+ }
178
+ }
179
+ Arguments.makeNativeMap(mapOf("accepted" to admission.complete(context, 1, batch, results)))
180
+ }
181
+
182
+ @ReactMethod fun remove(id: String, promise: Promise) = action(promise) {
183
+ if (installation == null && id == lastRemoved) return@action Arguments.makeNativeMap(lastRemoval ?: invalid())
184
+ if (id != installation) invalid()
185
+ Arguments.makeNativeMap(shutdown())
186
+ }
187
+
188
+ private fun current(id: String): AdmissionEngine {
189
+ if (installation != id || invalidated) invalid()
190
+ val admission = engine ?: invalid()
191
+ if (!admission.checkOwnership()) invalid()
192
+ return admission
193
+ }
194
+ private fun action(promise: Promise, block: () -> Any) {
195
+ scheduler.dispatch {
196
+ try { promise.resolve(block()) }
197
+ catch (_: Exception) { reject(promise) }
198
+ }
199
+ }
200
+ private fun reject(promise: Promise) { promise.reject("NATIVE_ADMISSION_UNAVAILABLE", "Native resource admission failed") }
201
+ private fun emit(event: Map<String, Any>) {
202
+ when (event["kind"]) {
203
+ "retired" -> (event["context"] as? String)?.let {
204
+ bootstrapRequests?.retire(it)
205
+ reactApplicationContext.getNativeModule(TileflowNativeDocumentsModule::class.java)?.retireNativeContext(it)
206
+ }
207
+ "ownershipLost" -> bootstrapRequests?.close()
208
+ "lifecycle" -> bootstrapRequests?.lifecycle(event["foreground"] == true)
209
+ }
210
+ if (listeners <= 0 || invalidated) throw IllegalStateException("Native admission listener is unavailable")
211
+ reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
212
+ .emit("TileflowNativeAdmissionEvent", Arguments.makeNativeMap(event))
213
+ }
214
+
215
+ override fun onHostPause() { scheduler.dispatch { engine?.lifecycle(false) } }
216
+ override fun onHostResume() { scheduler.dispatch { engine?.lifecycle(true) } }
217
+ override fun onHostDestroy() { scheduler.dispatch { invalidated = true; teardown() } }
218
+ override fun invalidate() {
219
+ reactApplicationContext.removeLifecycleEventListener(this)
220
+ scheduler.dispatch { invalidated = true; teardown() }
221
+ super.invalidate()
222
+ }
223
+ private fun teardown() {
224
+ try { shutdown() } catch (_: Exception) { /* Retain failed cleanup for retry; never report success. */ }
225
+ }
226
+ private fun shutdown(): Map<String, Boolean> {
227
+ val id = installation ?: return lastRemoval ?: mapOf("removed" to false, "ownershipLost" to false)
228
+ bootstrapRequests?.close(); engine?.close(); network?.close(); bootstrapNetwork?.close()
229
+ val ack = (lease ?: invalid()).remove()
230
+ lastRemoved = id; lastRemoval = ack
231
+ installation = null; engine = null; network = null; lease = null
232
+ bootstrapRequests = null; bootstrapNetwork = null
233
+ return ack
234
+ }
235
+ private fun invalid(): Nothing = throw IllegalArgumentException("Invalid native admission")
236
+ }
@@ -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,82 @@
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
+ import com.facebook.react.module.annotations.ReactModule
12
+
13
+ @ReactModule(name = "TileflowNativeDocuments")
14
+ class TileflowNativeDocumentsModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(context), LifecycleEventListener {
15
+ private val scheduler = HandlerAdmissionScheduler()
16
+ private val network = NativeDocumentOkHttpNetwork()
17
+ private val registry = NativeDocumentRegistry(scheduler, { url, limit, scope, developmentOrigin, active, completion ->
18
+ if (scope == null) network.start(url, limit, active, completion, developmentOrigin)
19
+ else scope.load(url, completion)
20
+ })
21
+ @Volatile private var invalidated = false
22
+
23
+ init {
24
+ registry.lifecycle(context.lifecycleState == LifecycleState.RESUMED)
25
+ context.addLifecycleEventListener(this)
26
+ }
27
+ override fun getName() = "TileflowNativeDocuments"
28
+
29
+ @ReactMethod fun openDocument(url: String, maximumBytes: Double, installation: String?, context: String?, developmentOrigin: String?, promise: Promise) = action(promise) {
30
+ if (invalidated || !maximumBytes.isFinite() || maximumBytes != maximumBytes.toInt().toDouble() || maximumBytes < 1 || maximumBytes > 8388608 ||
31
+ (installation == null) != (context == null)) invalid()
32
+ val scope = if (installation == null) null else {
33
+ if (!AdmissionUrl.validToken(installation) || !AdmissionUrl.validToken(context!!)) invalid()
34
+ val admission = reactApplicationContext.getNativeModule(TileflowNativeAdmissionModule::class.java) ?: invalid()
35
+ admission.documentScope(installation!!, context!!, maximumBytes.toInt())
36
+ }
37
+ Arguments.makeNativeMap(mapOf("document" to registry.open(url, maximumBytes.toInt(), scope, developmentOrigin)))
38
+ }
39
+
40
+ @ReactMethod fun documentResponse(document: String, promise: Promise) {
41
+ scheduler.dispatch {
42
+ try {
43
+ if (invalidated || !AdmissionUrl.validToken(document)) invalid()
44
+ registry.response(document) { response ->
45
+ if (response == null || invalidated) reject(promise)
46
+ else promise.resolve(Arguments.makeNativeMap(mapOf("url" to response.url, "status" to response.status)))
47
+ }
48
+ } catch (_: Exception) { reject(promise) }
49
+ }
50
+ }
51
+
52
+ @ReactMethod fun documentChunk(document: String, maximumBytes: Double, promise: Promise) = action(promise) {
53
+ if (invalidated || !maximumBytes.isFinite() || maximumBytes != maximumBytes.toInt().toDouble() || maximumBytes < 1 || maximumBytes > 65536) invalid()
54
+ val chunk = registry.chunk(document, maximumBytes.toInt())
55
+ val encoded = Base64.encodeToString(chunk.bytes, Base64.NO_WRAP)
56
+ chunk.bytes.fill(0)
57
+ Arguments.makeNativeMap(mapOf("bodyBase64" to encoded, "last" to chunk.last))
58
+ }
59
+
60
+ @ReactMethod fun cancelDocument(document: String, promise: Promise) = action(promise) {
61
+ registry.cancel(document)
62
+ Arguments.makeNativeMap(mapOf("cancelled" to true))
63
+ }
64
+
65
+ internal fun retireNativeContext(context: String) { registry.retireContext(context) }
66
+ private fun action(promise: Promise, block: () -> Any) {
67
+ scheduler.dispatch {
68
+ try { promise.resolve(block()) }
69
+ catch (_: Exception) { reject(promise) }
70
+ }
71
+ }
72
+ private fun reject(promise: Promise) { promise.reject("NATIVE_DOCUMENT_UNAVAILABLE", "Native document acquisition failed.") }
73
+ private fun invalid(): Nothing = throw IllegalArgumentException("Native document acquisition failed.")
74
+ override fun onHostPause() { scheduler.dispatch { registry.lifecycle(false) } }
75
+ override fun onHostResume() { scheduler.dispatch { if (!invalidated) registry.lifecycle(true) } }
76
+ override fun onHostDestroy() { scheduler.dispatch { invalidated = true; registry.close(); network.close() } }
77
+ override fun invalidate() {
78
+ reactApplicationContext.removeLifecycleEventListener(this)
79
+ scheduler.dispatch { invalidated = true; registry.close(); network.close() }
80
+ super.invalidate()
81
+ }
82
+ }