@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.
- package/README.md +216 -4
- package/TileflowNativeAdmission.podspec +34 -0
- package/android/build.gradle +43 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrap.kt +144 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrapOkHttpNetwork.kt +89 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionCatalog.kt +137 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionDocumentScope.kt +22 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionEngine.kt +335 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionModuleProvider.kt +68 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionOkHttpNetwork.kt +86 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionProviderLease.kt +56 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionTypes.kt +85 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionUrl.kt +100 -0
- package/android/src/main/java/dev/tileflow/reactnative/HandlerAdmissionScheduler.kt +16 -0
- package/android/src/main/java/dev/tileflow/reactnative/MobileConfiguration.kt +95 -0
- package/android/src/main/java/dev/tileflow/reactnative/MobileConfigurationResources.kt +43 -0
- package/android/src/main/java/dev/tileflow/reactnative/NativeDocumentOkHttpNetwork.kt +101 -0
- package/android/src/main/java/dev/tileflow/reactnative/NativeDocumentRegistry.kt +136 -0
- package/android/src/main/java/dev/tileflow/reactnative/NativeSurfaceState.kt +115 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionModule.kt +236 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionPackage.kt +16 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeConfigurationModule.kt +43 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeDocumentsModule.kt +82 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeSurfaceModule.kt +277 -0
- package/android/src/main/res/raw/tileflow_configuration_keep.xml +3 -0
- package/dist/contract-DWs-lDPo.d.ts +144 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +6989 -0
- package/dist/internal/camera.d.ts +65 -0
- package/dist/internal/camera.js +325 -0
- package/dist/internal/hosted-binding.d.ts +18 -0
- package/dist/internal/hosted-binding.js +277 -0
- package/dist/internal/initial-view.d.ts +11 -0
- package/dist/internal/initial-view.js +8 -0
- package/dist/internal/interactions.d.ts +76 -0
- package/dist/internal/interactions.js +794 -0
- package/dist/internal/native-admission-bridge.d.ts +29 -0
- package/dist/internal/native-admission-bridge.js +351 -0
- package/dist/internal/native-admission.d.ts +78 -0
- package/dist/internal/native-admission.js +1614 -0
- package/dist/internal/native-appearance.d.ts +21 -0
- package/dist/internal/native-appearance.js +110 -0
- package/dist/internal/native-configuration-bridge.d.ts +9 -0
- package/dist/internal/native-configuration-bridge.js +343 -0
- package/dist/internal/session.d.ts +125 -0
- package/dist/internal/session.js +887 -0
- package/dist/internal/source-state.d.ts +11 -0
- package/dist/internal/source-state.js +26 -0
- package/dist/native-admission-contract-k7LKE1vW.d.ts +89 -0
- package/docs/native-admission.md +108 -0
- package/docs/native-configuration.md +210 -0
- package/docs/native-interactions.md +161 -0
- package/docs/native-location.md +148 -0
- package/ios/TFAdmissionBootstrap.h +17 -0
- package/ios/TFAdmissionBootstrap.mm +168 -0
- package/ios/TFAdmissionCatalog.h +13 -0
- package/ios/TFAdmissionCatalog.mm +193 -0
- package/ios/TFAdmissionDocumentScope.h +7 -0
- package/ios/TFAdmissionDocumentScope.mm +45 -0
- package/ios/TFAdmissionEngine.h +57 -0
- package/ios/TFAdmissionEngine.mm +371 -0
- package/ios/TFAdmissionInstallation.h +27 -0
- package/ios/TFAdmissionInstallation.mm +117 -0
- package/ios/TFAdmissionNetwork.h +16 -0
- package/ios/TFAdmissionNetwork.mm +268 -0
- package/ios/TFAdmissionURL.mm +116 -0
- package/ios/TFDocumentBounds.h +9 -0
- package/ios/TFDocumentBounds.mm +19 -0
- package/ios/TFMobileConfiguration.h +23 -0
- package/ios/TFMobileConfiguration.mm +139 -0
- package/ios/TFNativeAdmissionURLProtocol.mm +69 -0
- package/ios/TFNativeDocumentRegistry.h +28 -0
- package/ios/TFNativeDocumentRegistry.mm +193 -0
- package/ios/TFNativeDocumentURL.h +6 -0
- package/ios/TFNativeSurfaceState.h +28 -0
- package/ios/TFNativeSurfaceState.mm +128 -0
- package/ios/TileflowNativeAdmission.h +9 -0
- package/ios/TileflowNativeAdmission.mm +184 -0
- package/ios/TileflowNativeConfiguration.mm +41 -0
- package/ios/TileflowNativeDocuments.h +12 -0
- package/ios/TileflowNativeDocuments.mm +105 -0
- package/ios/TileflowNativeSurface.mm +403 -0
- package/ios/tileflow_native_admission.rb +45 -0
- package/package.json +51 -7
- 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
|
+
}
|