@mentra/crust 3.3.0-dev.774 → 3.3.0-dev.780
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 +12 -0
- package/android/src/main/java/com/mentra/crust/CrustModule.kt +18 -6
- package/android/src/main/java/com/mentra/crust/http/NativeHttpRequests.kt +86 -0
- package/android/src/main/java/com/mentra/crust/jsc/JSCPolyfillBridge.kt +1 -53
- package/android/src/test/java/com/mentra/crust/http/NativeHttpRequestsTest.kt +133 -0
- package/build/CrustModule.d.ts +2 -1
- package/build/CrustModule.d.ts.map +1 -1
- package/build/CrustModule.js.map +1 -1
- package/build/CrustModule.web.d.ts +3 -1
- package/build/CrustModule.web.d.ts.map +1 -1
- package/build/CrustModule.web.js +20 -8
- package/build/CrustModule.web.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/nativeHttpRequest.d.ts +13 -0
- package/build/nativeHttpRequest.d.ts.map +1 -0
- package/build/nativeHttpRequest.js +51 -0
- package/build/nativeHttpRequest.js.map +1 -0
- package/build/nativeHttpRequest.test.d.ts +2 -0
- package/build/nativeHttpRequest.test.d.ts.map +1 -0
- package/build/nativeHttpRequest.test.js +88 -0
- package/build/nativeHttpRequest.test.js.map +1 -0
- package/package.json +2 -2
- package/src/CrustModule.ts +2 -0
- package/src/CrustModule.web.ts +18 -7
- package/src/index.ts +1 -0
- package/src/nativeHttpRequest.test.ts +94 -0
- package/src/nativeHttpRequest.ts +74 -0
package/README.md
CHANGED
|
@@ -46,3 +46,15 @@ sibling, which is declared as a dependency.
|
|
|
46
46
|
|
|
47
47
|
Source lives in the [MentraOS monorepo](https://github.com/Mentra-Community/MentraOS)
|
|
48
48
|
under `mobile/modules/crust`. Issues and contributions welcome there.
|
|
49
|
+
|
|
50
|
+
## Android native HTTP cancellation
|
|
51
|
+
|
|
52
|
+
Engine's cloud client uses `nativeHttpRequest(CrustModule, method, url, headers,
|
|
53
|
+
body, signal?)` from `@mentra/crust`. The helper preserves Android's native
|
|
54
|
+
background HTTP transport and connects the caller's `AbortSignal` to cancellation
|
|
55
|
+
of that exact OkHttp call. Request IDs and native call ownership stay inside the
|
|
56
|
+
transport. Completion removes its listener and native registry entry; module
|
|
57
|
+
shutdown cancels only that module's outstanding host requests.
|
|
58
|
+
|
|
59
|
+
Engine callers use the ordinary cloud-client API with a signal. They do not call
|
|
60
|
+
native start/cancel methods or maintain their own request registry.
|
|
@@ -22,6 +22,7 @@ import com.mentra.crust.preview.PixelCopyPreview
|
|
|
22
22
|
import com.mentra.crust.jsc.JSCRuntime
|
|
23
23
|
import com.mentra.crust.jsc.InstalledMiniappManifest
|
|
24
24
|
import com.mentra.crust.jsc.JSCPolyfillBridge
|
|
25
|
+
import com.mentra.crust.http.NativeHttpRequests
|
|
25
26
|
|
|
26
27
|
class CrustModule : Module() {
|
|
27
28
|
companion object {
|
|
@@ -99,6 +100,7 @@ class CrustModule : Module() {
|
|
|
99
100
|
)
|
|
100
101
|
private var notificationEventReceiver: BroadcastReceiver? = null
|
|
101
102
|
private var notificationBridgeContext: android.content.Context? = null
|
|
103
|
+
private val nativeHttpRequests = NativeHttpRequests(JSCPolyfillBridge.httpClient)
|
|
102
104
|
|
|
103
105
|
private fun registerNotificationBridgeIfPossible(): Boolean {
|
|
104
106
|
if (notificationEventReceiver != null) return true
|
|
@@ -169,6 +171,7 @@ class CrustModule : Module() {
|
|
|
169
171
|
notificationEventReceiver = null
|
|
170
172
|
notificationBridgeContext = null
|
|
171
173
|
eventEmitter = null
|
|
174
|
+
nativeHttpRequests.cancelAll()
|
|
172
175
|
mentraJsQueue.cancel()
|
|
173
176
|
mentraJsExecutor.shutdown()
|
|
174
177
|
}
|
|
@@ -187,12 +190,16 @@ class CrustModule : Module() {
|
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
AsyncFunction("nativeHttpRequest") {
|
|
190
|
-
method: String, url: String, headers: Map<String, String>, body: String?, promise: expo.modules.kotlin.Promise ->
|
|
191
|
-
|
|
192
|
-
method,
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
requestId: String, method: String, url: String, headers: Map<String, String>, body: String?, promise: expo.modules.kotlin.Promise ->
|
|
194
|
+
val request = try {
|
|
195
|
+
JSCPolyfillBridge.buildHttpRequest(method, url, headers, body)
|
|
196
|
+
} catch (error: Throwable) {
|
|
197
|
+
promise.reject("E_NATIVE_HTTP", error.message ?: "Native HTTP request failed", error)
|
|
198
|
+
return@AsyncFunction
|
|
199
|
+
}
|
|
200
|
+
nativeHttpRequests.enqueue(
|
|
201
|
+
requestId,
|
|
202
|
+
request,
|
|
196
203
|
onResult = { result ->
|
|
197
204
|
promise.resolve(
|
|
198
205
|
mapOf(
|
|
@@ -207,6 +214,11 @@ class CrustModule : Module() {
|
|
|
207
214
|
)
|
|
208
215
|
}
|
|
209
216
|
|
|
217
|
+
// Same serial Expo queue as nativeHttpRequest: cancellation cannot precede registration.
|
|
218
|
+
AsyncFunction("cancelNativeHttpRequest") { requestId: String ->
|
|
219
|
+
nativeHttpRequests.cancel(requestId)
|
|
220
|
+
}
|
|
221
|
+
|
|
210
222
|
Function("showAVRoutePicker") { _: String? ->
|
|
211
223
|
// iOS-only; Android uses system Bluetooth settings / Crust where appropriate.
|
|
212
224
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package com.mentra.crust.http
|
|
2
|
+
|
|
3
|
+
import java.io.IOException
|
|
4
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
5
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
6
|
+
import okhttp3.Call
|
|
7
|
+
import okhttp3.Callback
|
|
8
|
+
import okhttp3.Request
|
|
9
|
+
import okhttp3.Response
|
|
10
|
+
|
|
11
|
+
/** Calls owned by one Expo module, separate from miniapp fetch and WebSocket traffic. */
|
|
12
|
+
internal class NativeHttpRequests(private val client: Call.Factory) {
|
|
13
|
+
data class Result(
|
|
14
|
+
val status: Int,
|
|
15
|
+
val statusText: String,
|
|
16
|
+
val headers: Map<String, String>,
|
|
17
|
+
val body: String,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
private val calls = ConcurrentHashMap<String, Call>()
|
|
21
|
+
private val closed = AtomicBoolean(false)
|
|
22
|
+
internal val activeCount: Int get() = calls.size
|
|
23
|
+
|
|
24
|
+
fun enqueue(
|
|
25
|
+
requestId: String,
|
|
26
|
+
request: Request,
|
|
27
|
+
onResult: (Result) -> Unit,
|
|
28
|
+
onError: (Throwable) -> Unit,
|
|
29
|
+
) {
|
|
30
|
+
if (closed.get()) {
|
|
31
|
+
onError(IllegalStateException("Native HTTP module is closed"))
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
val call = client.newCall(request)
|
|
35
|
+
if (calls.putIfAbsent(requestId, call) != null) {
|
|
36
|
+
onError(IllegalArgumentException("Native HTTP request ID is already active"))
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
// Destruction may run on a different thread from Expo's serial request queue.
|
|
40
|
+
if (closed.get()) {
|
|
41
|
+
calls.remove(requestId, call)
|
|
42
|
+
call.cancel()
|
|
43
|
+
onError(IllegalStateException("Native HTTP module is closed"))
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
call.enqueue(object : Callback {
|
|
48
|
+
override fun onFailure(call: Call, e: IOException) {
|
|
49
|
+
calls.remove(requestId, call)
|
|
50
|
+
onError(e)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override fun onResponse(call: Call, response: Response) {
|
|
54
|
+
val result = try {
|
|
55
|
+
response.use { r ->
|
|
56
|
+
val headers = r.headers.names().associate { name ->
|
|
57
|
+
name.lowercase() to r.headers.values(name).joinToString(", ")
|
|
58
|
+
}
|
|
59
|
+
Result(r.code, r.message, headers, r.body?.string() ?: "")
|
|
60
|
+
}
|
|
61
|
+
} catch (e: Throwable) {
|
|
62
|
+
calls.remove(requestId, call)
|
|
63
|
+
onError(e)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
calls.remove(requestId, call)
|
|
67
|
+
onResult(result)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
} catch (e: Throwable) {
|
|
71
|
+
calls.remove(requestId, call)
|
|
72
|
+
onError(e)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fun cancel(requestId: String) {
|
|
77
|
+
calls.remove(requestId)?.cancel()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fun cancelAll() {
|
|
81
|
+
closed.set(true)
|
|
82
|
+
for ((requestId, call) in calls) {
|
|
83
|
+
if (calls.remove(requestId, call)) call.cancel()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -37,7 +37,7 @@ import android.util.Base64
|
|
|
37
37
|
object JSCPolyfillBridge {
|
|
38
38
|
private const val TAG = "MentraJS.PolyfillBridge"
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
internal val httpClient: OkHttpClient by lazy {
|
|
41
41
|
OkHttpClient.Builder()
|
|
42
42
|
.connectTimeout(30, TimeUnit.SECONDS)
|
|
43
43
|
.readTimeout(60, TimeUnit.SECONDS)
|
|
@@ -45,58 +45,6 @@ object JSCPolyfillBridge {
|
|
|
45
45
|
.build()
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
data class HttpResult(
|
|
49
|
-
val status: Int,
|
|
50
|
-
val statusText: String,
|
|
51
|
-
val headers: Map<String, String>,
|
|
52
|
-
val body: String,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Shared OkHttp path for host cloud-client requests. Asynchronous because Expo runs every
|
|
57
|
-
* module's AsyncFunction on one shared thread: a blocking call here stalls miniapp delivery
|
|
58
|
-
* and every other native module for the length of the request, up to [callTimeout].
|
|
59
|
-
*/
|
|
60
|
-
fun enqueueHttp(
|
|
61
|
-
method: String,
|
|
62
|
-
url: String,
|
|
63
|
-
headers: Map<String, String>,
|
|
64
|
-
bodyString: String?,
|
|
65
|
-
onResult: (HttpResult) -> Unit,
|
|
66
|
-
onError: (Throwable) -> Unit,
|
|
67
|
-
) {
|
|
68
|
-
val request = try {
|
|
69
|
-
buildHttpRequest(method, url, headers, bodyString)
|
|
70
|
-
} catch (e: Throwable) {
|
|
71
|
-
onError(e)
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
httpClient.newCall(request).enqueue(object : Callback {
|
|
75
|
-
override fun onFailure(call: Call, e: IOException) = onError(e)
|
|
76
|
-
|
|
77
|
-
override fun onResponse(call: Call, response: Response) {
|
|
78
|
-
try {
|
|
79
|
-
response.use { r ->
|
|
80
|
-
val responseHeaders = mutableMapOf<String, String>()
|
|
81
|
-
for (name in r.headers.names()) {
|
|
82
|
-
responseHeaders[name.lowercase()] = r.headers.values(name).joinToString(", ")
|
|
83
|
-
}
|
|
84
|
-
onResult(
|
|
85
|
-
HttpResult(
|
|
86
|
-
status = r.code,
|
|
87
|
-
statusText = r.message,
|
|
88
|
-
headers = responseHeaders,
|
|
89
|
-
body = r.body?.string() ?: "",
|
|
90
|
-
)
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
} catch (e: Throwable) {
|
|
94
|
-
onError(e)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
|
|
100
48
|
/** Keep host HTTP and miniapp fetch body semantics identical, including empty POSTs. */
|
|
101
49
|
internal fun buildHttpRequest(method: String, url: String, headers: Map<String, String>, bodyString: String?): Request {
|
|
102
50
|
val builder = Request.Builder().url(url)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
package com.mentra.crust.http
|
|
2
|
+
|
|
3
|
+
import java.io.IOException
|
|
4
|
+
import java.net.ServerSocket
|
|
5
|
+
import java.util.concurrent.CountDownLatch
|
|
6
|
+
import java.util.concurrent.TimeUnit
|
|
7
|
+
import okhttp3.Call
|
|
8
|
+
import okhttp3.Callback
|
|
9
|
+
import okhttp3.OkHttpClient
|
|
10
|
+
import okhttp3.Protocol
|
|
11
|
+
import okhttp3.Request
|
|
12
|
+
import okhttp3.Response
|
|
13
|
+
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
14
|
+
import okio.Timeout
|
|
15
|
+
import org.junit.Assert.*
|
|
16
|
+
import org.junit.Test
|
|
17
|
+
|
|
18
|
+
class NativeHttpRequestsTest {
|
|
19
|
+
private val request = Request.Builder().url("https://example.com/report").build()
|
|
20
|
+
|
|
21
|
+
private class PendingCall(private val request: Request) : Call {
|
|
22
|
+
var callback: Callback? = null
|
|
23
|
+
var cancelled = false
|
|
24
|
+
var onEnqueue: (() -> Unit)? = null
|
|
25
|
+
override fun request() = request
|
|
26
|
+
override fun execute(): Response = error("Test only exercises asynchronous calls")
|
|
27
|
+
override fun enqueue(responseCallback: Callback) {
|
|
28
|
+
callback = responseCallback
|
|
29
|
+
onEnqueue?.invoke()
|
|
30
|
+
}
|
|
31
|
+
override fun cancel() {cancelled = true}
|
|
32
|
+
override fun isExecuted() = callback != null
|
|
33
|
+
override fun isCanceled() = cancelled
|
|
34
|
+
override fun timeout() = Timeout.NONE
|
|
35
|
+
override fun clone(): Call = PendingCall(request)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Test fun `register before enqueue and cancel only the requested call`() {
|
|
39
|
+
val calls = mutableListOf<PendingCall>()
|
|
40
|
+
val owner = NativeHttpRequests(Call.Factory { req -> PendingCall(req).also {calls.add(it)} })
|
|
41
|
+
owner.enqueue("one", request, {}, {throw it})
|
|
42
|
+
calls[0].onEnqueue = {assertEquals(2, owner.activeCount)}
|
|
43
|
+
owner.enqueue("two", request, {}, {throw it})
|
|
44
|
+
assertEquals(2, owner.activeCount)
|
|
45
|
+
owner.cancel("one")
|
|
46
|
+
assertTrue(calls[0].cancelled)
|
|
47
|
+
assertFalse(calls[1].cancelled)
|
|
48
|
+
assertEquals(1, owner.activeCount)
|
|
49
|
+
owner.cancel("not-owned")
|
|
50
|
+
assertEquals(1, owner.activeCount)
|
|
51
|
+
owner.cancelAll()
|
|
52
|
+
assertTrue(calls[1].cancelled)
|
|
53
|
+
assertEquals(0, owner.activeCount)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Test fun `completion removes the active request and preserves the response`() {
|
|
57
|
+
val call = PendingCall(request)
|
|
58
|
+
val owner = NativeHttpRequests(Call.Factory {call})
|
|
59
|
+
var result: NativeHttpRequests.Result? = null
|
|
60
|
+
call.onEnqueue = {assertEquals(1, owner.activeCount)}
|
|
61
|
+
owner.enqueue("complete", request, {result = it}, {throw it})
|
|
62
|
+
call.callback!!.onResponse(call, Response.Builder().request(request).protocol(Protocol.HTTP_1_1)
|
|
63
|
+
.code(200).message("OK").header("Content-Type", "application/json").body("{}".toResponseBody()).build())
|
|
64
|
+
assertEquals("{}", result!!.body)
|
|
65
|
+
assertEquals("application/json", result!!.headers["content-type"])
|
|
66
|
+
assertEquals(0, owner.activeCount)
|
|
67
|
+
owner.cancel("complete")
|
|
68
|
+
assertFalse(call.cancelled)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test fun `network failure and synchronous enqueue failure leave no registry entries`() {
|
|
72
|
+
val call = PendingCall(request)
|
|
73
|
+
val owner = NativeHttpRequests(Call.Factory {call})
|
|
74
|
+
var failures = 0
|
|
75
|
+
owner.enqueue("failed", request, {}, {failures++})
|
|
76
|
+
call.callback!!.onFailure(call, IOException("offline"))
|
|
77
|
+
assertEquals(0, owner.activeCount)
|
|
78
|
+
call.onEnqueue = {throw IllegalStateException("dispatcher closed")}
|
|
79
|
+
owner.enqueue("enqueue-failed", request, {}, {failures++})
|
|
80
|
+
assertEquals(0, owner.activeCount)
|
|
81
|
+
assertEquals(2, failures)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test fun `duplicate IDs cannot replace existing ownership and destruction prevents later requests`() {
|
|
85
|
+
val calls = mutableListOf<PendingCall>()
|
|
86
|
+
val owner = NativeHttpRequests(Call.Factory {req -> PendingCall(req).also {calls.add(it)} })
|
|
87
|
+
var failures = 0
|
|
88
|
+
owner.enqueue("one", request, {}, {failures++})
|
|
89
|
+
owner.enqueue("one", request, {}, {failures++})
|
|
90
|
+
assertEquals(1, failures)
|
|
91
|
+
assertEquals(1, owner.activeCount)
|
|
92
|
+
assertNull(calls[1].callback)
|
|
93
|
+
owner.cancelAll()
|
|
94
|
+
assertTrue(calls[0].cancelled)
|
|
95
|
+
owner.enqueue("later", request, {}, {failures++})
|
|
96
|
+
assertEquals(2, failures)
|
|
97
|
+
assertEquals(2, calls.size)
|
|
98
|
+
assertEquals(0, owner.activeCount)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Test fun `cancel interrupts an actual OkHttp response body and closes the server socket`() {
|
|
102
|
+
val client = OkHttpClient()
|
|
103
|
+
val owner = NativeHttpRequests(client)
|
|
104
|
+
val bodyStarted = CountDownLatch(1)
|
|
105
|
+
val socketClosed = CountDownLatch(1)
|
|
106
|
+
val failed = CountDownLatch(1)
|
|
107
|
+
ServerSocket(0, 1, java.net.InetAddress.getLoopbackAddress()).use {server ->
|
|
108
|
+
val thread = Thread {
|
|
109
|
+
server.accept().use {socket ->
|
|
110
|
+
socket.soTimeout = 5_000
|
|
111
|
+
val reader = socket.getInputStream().bufferedReader()
|
|
112
|
+
while (!reader.readLine().isNullOrEmpty()) {}
|
|
113
|
+
socket.getOutputStream().write("HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n".toByteArray())
|
|
114
|
+
socket.getOutputStream().flush()
|
|
115
|
+
bodyStarted.countDown()
|
|
116
|
+
if (socket.getInputStream().read() == -1) socketClosed.countDown()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
thread.isDaemon = true
|
|
120
|
+
thread.start()
|
|
121
|
+
val req = Request.Builder().url("http://127.0.0.1:${server.localPort}/report").build()
|
|
122
|
+
owner.enqueue("stalled-body", req, {fail("Cancelled body must not complete")}, {failed.countDown()})
|
|
123
|
+
assertTrue("server started body", bodyStarted.await(5, TimeUnit.SECONDS))
|
|
124
|
+
owner.cancel("stalled-body")
|
|
125
|
+
assertTrue("native request rejected", failed.await(5, TimeUnit.SECONDS))
|
|
126
|
+
assertTrue("socket closed", socketClosed.await(5, TimeUnit.SECONDS))
|
|
127
|
+
assertEquals(0, owner.activeCount)
|
|
128
|
+
thread.join(5_000)
|
|
129
|
+
}
|
|
130
|
+
client.dispatcher.executorService.shutdownNow()
|
|
131
|
+
client.connectionPool.evictAll()
|
|
132
|
+
}
|
|
133
|
+
}
|
package/build/CrustModule.d.ts
CHANGED
|
@@ -8,12 +8,13 @@ declare class CrustModule extends NativeModule<CrustModuleEvents> {
|
|
|
8
8
|
setValueAsync(value: string): Promise<void>;
|
|
9
9
|
/** Android: copy the displayed view's window pixels to a temporary JPEG URI. */
|
|
10
10
|
captureMiniappPreview(viewTag: number): Promise<string>;
|
|
11
|
-
nativeHttpRequest(method: string, url: string, headers: Record<string, string>, body?: string | null): Promise<{
|
|
11
|
+
nativeHttpRequest(requestId: string, method: string, url: string, headers: Record<string, string>, body?: string | null): Promise<{
|
|
12
12
|
status: number;
|
|
13
13
|
statusText: string;
|
|
14
14
|
headers: Record<string, string>;
|
|
15
15
|
body: string;
|
|
16
16
|
}>;
|
|
17
|
+
cancelNativeHttpRequest(requestId: string): Promise<void>;
|
|
17
18
|
showAVRoutePicker(tintColor?: string | null): void;
|
|
18
19
|
/**
|
|
19
20
|
* iOS: configure `preferredScreenEdgesDeferringSystemGestures`. When an
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CrustModule.d.ts","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CrustModule.d.ts","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"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"}
|
package/build/CrustModule.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CrustModule.js","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"CrustModule.js","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;AAoMtD,yDAAyD;AACzD,eAAe,mBAAmB,CAAc,OAAO,CAAC,CAAA","sourcesContent":["import {NativeModule, requireNativeModule} from \"expo\"\n\nimport {CrustModuleEvents, InstalledApp} from \"./Crust.types\"\n\ndeclare class CrustModule extends NativeModule<CrustModuleEvents> {\n /** True for the native iOS app running on an Apple Silicon Mac. */\n readonly isIOSAppOnMac?: boolean\n PI: number\n hello(): string\n setValueAsync(value: string): Promise<void>\n /** Android: copy the displayed view's window pixels to a temporary JPEG URI. */\n captureMiniappPreview(viewTag: number): Promise<string>\n nativeHttpRequest(\n requestId: string,\n method: string,\n url: string,\n headers: Record<string, string>,\n body?: string | null,\n ): Promise<{status: number; statusText: string; headers: Record<string, string>; body: string}>\n cancelNativeHttpRequest(requestId: string): Promise<void>\n showAVRoutePicker(tintColor?: string | null): void\n\n /**\n * iOS: configure `preferredScreenEdgesDeferringSystemGestures`. When an\n * edge is deferred, the first swipe across that edge is consumed by the\n * app and the system gesture (Control Center, Notification Center, Home\n * indicator) only fires on a second swipe — i.e. a two-swipe-to-exit UX.\n *\n * Pass `[]` to restore default behavior. Android: no-op (Android has no\n * per-app equivalent; system gestures are configured at the OS level).\n */\n setDeferredSystemGestures(edges: Array<\"top\" | \"bottom\" | \"left\" | \"right\" | \"all\">): Promise<void>\n\n // MentraOS Notification Commands\n setNotificationConfig(listenerEnabled: boolean, blocklist: string[]): Promise<void>\n getInstalledApps(): Promise<InstalledApp[]>\n getInstalledAppsForNotifications(): Promise<InstalledApp[]>\n hasNotificationListenerPermission(): Promise<boolean>\n refreshNotificationListener(): Promise<boolean>\n openNotificationListenerSettings(): Promise<boolean>\n isBetaBuild(): Promise<boolean>\n // location services commands\n showLocationServicesDialog(): Promise<boolean>\n isLocationServicesEnabled(): Promise<boolean>\n openLocationSettings(): Promise<boolean>\n openAppSettings(): Promise<boolean>\n openBluetoothSettings(): Promise<boolean>\n\n // Image Processing Commands\n processGalleryImage(\n inputPath: string,\n outputPath: string,\n options: {\n lensCorrection?: boolean\n colorCorrection?: boolean\n },\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n mergeHdrBrackets(\n underPath: string,\n normalPath: string,\n overPath: string,\n outputPath: string,\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n stabilizeVideo(\n inputPath: string,\n imuPath: string,\n outputPath: string,\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n // Media Library Commands\n saveToGalleryWithDate(\n filePath: string,\n captureTimeMillis?: number,\n displayName?: string,\n ): Promise<{\n success: boolean\n uri?: string\n identifier?: string\n existing?: boolean\n error?: string\n }>\n\n // Navigation (Android only — iOS stubs return error)\n startNavigation(\n lat: number,\n lng: number,\n options?: {\n simulate?: boolean\n speedMultiplier?: number\n /** Optional multi-stop list. When present takes precedence over lat/lng. Last entry is the final destination. */\n stops?: Array<{lat: number; lng: number}>\n /** \"walking\" | \"driving\" | \"cycling\" | \"two_wheeler\". Defaults driving. */\n mode?: string\n avoid?: {highways?: boolean; tolls?: boolean; ferries?: boolean}\n /** Force a reroute when the user is more than N meters past a pivot they didn't take. */\n missedTurnRerouteMeters?: number\n },\n ): Promise<{ok: boolean; error?: string}>\n stopNavigation(): Promise<{ok: boolean; error?: string}>\n\n /**\n * Show the Google Nav SDK Terms & Conditions dialog if not already\n * accepted. Idempotent — resolves immediately with `{accepted: true}`\n * when the user has already accepted (cached in-process / on-disk /\n * inside the SDK).\n */\n requestNavigationPermission(): Promise<{ok: boolean; accepted: boolean; error?: string}>\n\n /**\n * Dev-only: clear cached T&C acceptance (SDK flag + on-disk pref +\n * in-process flag) so the next requestNavigationPermission() re-shows\n * the dialog. Android only; iOS returns {ok: false, error: \"not\n * supported on iOS\"}.\n */\n resetNavigationPermission(): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev-only: nudge the simulated user position ~offsetMeters perpendicular\n * to the route so the Nav SDK reroutes. Default 20m. Android only.\n */\n simulateDeviation(offsetMeters?: number): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev toggle. When enabled, the native NavigationManager shifts every\n * reported location ~8m perpendicular to the route bearing, simulating\n * a pedestrian walking on the wrong sidewalk. Only meaningful in\n * simulate mode; lets us verify the SDK's along-path pivot trigger\n * fires even when the user never crosses the 7m pivot point radius.\n * Android-only today; iOS is a no-op stub.\n */\n setWrongSidewalkOffset(enabled: boolean): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev toggle. When enabled, the native NavigationManager takes over\n * from the Google simulator and walks the user along a modified\n * polyline that omits crossing micro-steps — reproducing the\n * wrong-sidewalk-then-missed-the-turn scenario. Android-only today;\n * iOS is a no-op stub.\n */\n setSkipCrossings(enabled: boolean): Promise<{ok: boolean; error?: string}>\n\n // Heading / compass (Android only)\n startHeading(): Promise<{ok: boolean; error?: string}>\n stopHeading(): Promise<{ok: boolean; error?: string}>\n\n // MentraJS Runtime — per-miniapp JSContext lifecycle.\n /**\n * Spawn a per-miniapp JS context. Re-spawn is allowed: a live context\n * with the same packageName is killed first. Returns true if the\n * polyfill bundle + miniapp source evaluated without throwing.\n *\n * The polyfill bundle is `mobile/modules/mentrajs-runtime/dist/startup.js`\n * (shipped inside the host binary). It installs window-style globals\n * (console, timers, fetch, localStorage, crypto) atop the JSC runtime.\n */\n mentraJsSpawn(packageName: string, polyfillBundle: string, miniappJs: string): Promise<boolean>\n mentraJsEvaluate(packageName: string, source: string): Promise<unknown>\n mentraJsKill(packageName: string): Promise<void>\n /**\n * Push a `{kind: \"event\"|\"response\", …}` envelope into the named\n * context's globalThis.__deliver. Returns when the underlying\n * evaluateScript completes (the JS handler runs synchronously on the\n * context's queue).\n */\n mentraJsDispatchToJs(packageName: string, envelope: Record<string, unknown>): Promise<void>\n mentraJsSetManifest(packageName: string, permissions: string[]): Promise<void>\n /** Diagnostic — returns the packageNames of every live JSContext. */\n mentraJsAlivePackages(): string[]\n /** Diagnostic — force a GC cycle on the named context. */\n mentraJsDebugForceGC(packageName: string): Promise<boolean>\n /**\n * Read the bundled MentraJS polyfill (startup.js) shipped inside the\n * host binary. Synchronous — host RN code calls this once on app boot\n * and caches the string, then passes it to every mentraJsSpawn so\n * every JSContext starts with the same polyfill ABI.\n */\n mentraJsLoadPolyfillBundle(): string\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule<CrustModule>(\"Crust\")\n"]}
|
|
@@ -2,8 +2,9 @@ import { NativeModule } from "expo";
|
|
|
2
2
|
import { CrustModuleEvents } from "./Crust.types";
|
|
3
3
|
declare class CrustModule extends NativeModule<CrustModuleEvents> {
|
|
4
4
|
PI: number;
|
|
5
|
+
private readonly httpRequests;
|
|
5
6
|
setValueAsync(value: string): Promise<void>;
|
|
6
|
-
nativeHttpRequest(method: string, url: string, headers: Record<string, string>, body?: string | null): Promise<{
|
|
7
|
+
nativeHttpRequest(requestId: string, method: string, url: string, headers: Record<string, string>, body?: string | null): Promise<{
|
|
7
8
|
status: number;
|
|
8
9
|
statusText: string;
|
|
9
10
|
headers: {
|
|
@@ -11,6 +12,7 @@ declare class CrustModule extends NativeModule<CrustModuleEvents> {
|
|
|
11
12
|
};
|
|
12
13
|
body: string;
|
|
13
14
|
}>;
|
|
15
|
+
cancelNativeHttpRequest(requestId: string): Promise<void>;
|
|
14
16
|
hello(): string;
|
|
15
17
|
showAVRoutePicker(_tintColor?: string | null): void;
|
|
16
18
|
setDeferredSystemGestures(_edges: string[]): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CrustModule.web.d.ts","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,YAAY,EAAC,MAAM,MAAM,CAAA;AAEpD,OAAO,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAA;AAE/C,cAAM,WAAY,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACvD,EAAE,SAAU;
|
|
1
|
+
{"version":3,"file":"CrustModule.web.d.ts","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,YAAY,EAAC,MAAM,MAAM,CAAA;AAEpD,OAAO,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAA;AAE/C,cAAM,WAAY,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACvD,EAAE,SAAU;IACZ,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqC;IAC5D,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAG3C,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;;;;;;;;IAevH,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/D,KAAK;IAGL,iBAAiB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;IACtC,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1D,qBAAqB,CAAC,gBAAgB,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IACrF,gBAAgB;IAGhB,gCAAgC;IAGhC,iCAAiC;IAGjC,2BAA2B;IAG3B,gCAAgC;IAGhC,WAAW;IAGX,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;IAGjE,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAG3C,YAAY,CAAC,IAAI,EAAE,MAAM;IAGzB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAGhE,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE;IAGxD,qBAAqB;IAGf,oBAAoB,CAAC,IAAI,EAAE,MAAM;IAGvC,0BAA0B;CAG3B;;AAED,wBAA4D"}
|
package/build/CrustModule.web.js
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
import { registerWebModule, NativeModule } from "expo";
|
|
2
2
|
class CrustModule extends NativeModule {
|
|
3
3
|
PI = Math.PI;
|
|
4
|
+
httpRequests = new Map();
|
|
4
5
|
async setValueAsync(value) {
|
|
5
6
|
this.emit("onChange", { value });
|
|
6
7
|
}
|
|
7
|
-
async nativeHttpRequest(method, url, headers, body) {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
async nativeHttpRequest(requestId, method, url, headers, body) {
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
this.httpRequests.set(requestId, controller);
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch(url, { method, headers, body, signal: controller.signal });
|
|
13
|
+
return {
|
|
14
|
+
status: response.status,
|
|
15
|
+
statusText: response.statusText,
|
|
16
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
17
|
+
body: await response.text(),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
this.httpRequests.delete(requestId);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async cancelNativeHttpRequest(requestId) {
|
|
25
|
+
this.httpRequests.get(requestId)?.abort();
|
|
26
|
+
this.httpRequests.delete(requestId);
|
|
15
27
|
}
|
|
16
28
|
hello() {
|
|
17
29
|
return "Hello world! 👋";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CrustModule.web.js","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,iBAAiB,EAAE,YAAY,EAAC,MAAM,MAAM,CAAA;AAIpD,MAAM,WAAY,SAAQ,YAA+B;IACvD,EAAE,GAAG,IAAI,CAAC,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"CrustModule.web.js","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"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","sourcesContent":["import {registerWebModule, NativeModule} from \"expo\"\n\nimport {CrustModuleEvents} from \"./Crust.types\"\n\nclass CrustModule extends NativeModule<CrustModuleEvents> {\n PI = Math.PI\n private readonly httpRequests = new Map<string, AbortController>()\n async setValueAsync(value: string): Promise<void> {\n this.emit(\"onChange\", {value})\n }\n async nativeHttpRequest(requestId: string, method: string, url: string, headers: Record<string, string>, body?: string | null) {\n const controller = new AbortController()\n this.httpRequests.set(requestId, controller)\n try {\n const response = await fetch(url, {method, headers, body, signal: controller.signal})\n return {\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n body: await response.text(),\n }\n } finally {\n this.httpRequests.delete(requestId)\n }\n }\n async cancelNativeHttpRequest(requestId: string): Promise<void> {\n this.httpRequests.get(requestId)?.abort()\n this.httpRequests.delete(requestId)\n }\n hello() {\n return \"Hello world! 👋\"\n }\n showAVRoutePicker(_tintColor?: string | null) {}\n async setDeferredSystemGestures(_edges: string[]): Promise<void> {}\n async setNotificationConfig(_listenerEnabled: boolean, _blocklist: string[]): Promise<void> {}\n async getInstalledApps() {\n return []\n }\n async getInstalledAppsForNotifications() {\n return []\n }\n async hasNotificationListenerPermission() {\n return false\n }\n async refreshNotificationListener() {\n return false\n }\n async openNotificationListenerSettings() {\n return false\n }\n async isBetaBuild() {\n return false\n }\n async mentraJsSpawn(_pkg: string, _polyfill: string, _miniappJs: string) {\n return false\n }\n async mentraJsEvaluate(_pkg: string, _src: string) {\n return null\n }\n async mentraJsKill(_pkg: string) {\n return\n }\n async mentraJsDispatchToJs(_pkg: string, _env: Record<string, unknown>) {\n return\n }\n async mentraJsSetManifest(_pkg: string, _perms: string[]) {\n return\n }\n mentraJsAlivePackages() {\n return []\n }\n async mentraJsDebugForceGC(_pkg: string) {\n return false\n }\n mentraJsLoadPolyfillBundle() {\n return \"\"\n }\n}\n\nexport default registerWebModule(CrustModule, \"CrustModule\")\n"]}
|
package/build/index.d.ts
CHANGED
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAA;AACrC,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,aAAa,CAAA;AAChD,cAAc,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAA;AACrC,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,aAAa,CAAA;AAChD,cAAc,eAAe,CAAA;AAC7B,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAA"}
|
package/build/index.js
CHANGED
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,4CAA4C;AAC5C,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAA;AACrC,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,aAAa,CAAA;AAChD,cAAc,eAAe,CAAA","sourcesContent":["// Reexport the native module. On web, it will be resolved to CrustModule.web.ts\n// and on native platforms to CrustModule.ts\nexport {default} from \"./CrustModule\"\nexport {default as CrustView} from \"./CrustView\"\nexport * from \"./Crust.types\"\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,4CAA4C;AAC5C,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAA;AACrC,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,aAAa,CAAA;AAChD,cAAc,eAAe,CAAA;AAC7B,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAA","sourcesContent":["// Reexport the native module. On web, it will be resolved to CrustModule.web.ts\n// and on native platforms to CrustModule.ts\nexport {default} from \"./CrustModule\"\nexport {default as CrustView} from \"./CrustView\"\nexport * from \"./Crust.types\"\nexport {nativeHttpRequest} from \"./nativeHttpRequest\"\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface NativeHttpResult {
|
|
2
|
+
status: number;
|
|
3
|
+
statusText: string;
|
|
4
|
+
headers: Record<string, string>;
|
|
5
|
+
body: string;
|
|
6
|
+
}
|
|
7
|
+
export interface NativeHttpTransport {
|
|
8
|
+
nativeHttpRequest(requestId: string, method: string, url: string, headers: Record<string, string>, body: string | null): Promise<NativeHttpResult>;
|
|
9
|
+
cancelNativeHttpRequest(requestId: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
/** Keep Android's background-safe HTTP path while cancelling its actual native request. */
|
|
12
|
+
export declare function nativeHttpRequest(native: NativeHttpTransport, method: string, url: string, headers: Record<string, string>, body: string | null, signal?: AbortSignal): Promise<NativeHttpResult>;
|
|
13
|
+
//# sourceMappingURL=nativeHttpRequest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nativeHttpRequest.d.ts","sourceRoot":"","sources":["../src/nativeHttpRequest.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,mBAAmB;IAClC,iBAAiB,CACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,IAAI,EAAE,MAAM,GAAG,IAAI,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC5B,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1D;AAWD,2FAA2F;AAC3F,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,mBAAmB,EAC3B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAsC3B"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
let requestSequence = 0;
|
|
2
|
+
function abortReason(signal) {
|
|
3
|
+
if (signal.reason !== undefined)
|
|
4
|
+
return signal.reason;
|
|
5
|
+
const error = new Error("Native HTTP request aborted");
|
|
6
|
+
error.name = "AbortError";
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
/** Keep Android's background-safe HTTP path while cancelling its actual native request. */
|
|
10
|
+
export function nativeHttpRequest(native, method, url, headers, body, signal) {
|
|
11
|
+
if (signal?.aborted)
|
|
12
|
+
return Promise.reject(abortReason(signal));
|
|
13
|
+
const requestId = `http-${Date.now().toString(36)}-${++requestSequence}`;
|
|
14
|
+
// Start first, then subscribe: Expo delivers start and cancel on the same serial queue.
|
|
15
|
+
const request = native.nativeHttpRequest(requestId, method, url, headers, body);
|
|
16
|
+
if (!signal)
|
|
17
|
+
return request;
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
let settled = false;
|
|
20
|
+
let cancelling = false;
|
|
21
|
+
const finish = (action) => {
|
|
22
|
+
if (settled)
|
|
23
|
+
return;
|
|
24
|
+
settled = true;
|
|
25
|
+
signal.removeEventListener("abort", onAbort);
|
|
26
|
+
action();
|
|
27
|
+
};
|
|
28
|
+
const onAbort = () => {
|
|
29
|
+
if (settled || cancelling)
|
|
30
|
+
return;
|
|
31
|
+
cancelling = true;
|
|
32
|
+
void native.cancelNativeHttpRequest(requestId).then(() => finish(() => reject(abortReason(signal))), (error) => finish(() => reject(error)));
|
|
33
|
+
};
|
|
34
|
+
request.then((result) => {
|
|
35
|
+
if (signal.aborted)
|
|
36
|
+
onAbort();
|
|
37
|
+
else
|
|
38
|
+
finish(() => resolve(result));
|
|
39
|
+
}, (error) => {
|
|
40
|
+
if (signal.aborted)
|
|
41
|
+
onAbort();
|
|
42
|
+
else
|
|
43
|
+
finish(() => reject(error));
|
|
44
|
+
});
|
|
45
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
46
|
+
// Handles an abort fired synchronously during native registration.
|
|
47
|
+
if (signal.aborted)
|
|
48
|
+
onAbort();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=nativeHttpRequest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nativeHttpRequest.js","sourceRoot":"","sources":["../src/nativeHttpRequest.ts"],"names":[],"mappings":"AAkBA,IAAI,eAAe,GAAG,CAAC,CAAA;AAEvB,SAAS,WAAW,CAAC,MAAmB;IACtC,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,MAAM,CAAA;IACrD,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAA;IACtD,KAAK,CAAC,IAAI,GAAG,YAAY,CAAA;IACzB,OAAO,KAAK,CAAA;AACd,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,iBAAiB,CAC/B,MAA2B,EAC3B,MAAc,EACd,GAAW,EACX,OAA+B,EAC/B,IAAmB,EACnB,MAAoB;IAEpB,IAAI,MAAM,EAAE,OAAO;QAAE,OAAO,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAA;IAC/D,MAAM,SAAS,GAAG,QAAQ,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,CAAA;IACxE,wFAAwF;IACxF,MAAM,OAAO,GAAG,MAAM,CAAC,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;IAC/E,IAAI,CAAC,MAAM;QAAE,OAAO,OAAO,CAAA;IAE3B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,OAAO,GAAG,KAAK,CAAA;QACnB,IAAI,UAAU,GAAG,KAAK,CAAA;QACtB,MAAM,MAAM,GAAG,CAAC,MAAkB,EAAE,EAAE;YACpC,IAAI,OAAO;gBAAE,OAAM;YACnB,OAAO,GAAG,IAAI,CAAA;YACd,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YAC5C,MAAM,EAAE,CAAA;QACV,CAAC,CAAA;QACD,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,IAAI,OAAO,IAAI,UAAU;gBAAE,OAAM;YACjC,UAAU,GAAG,IAAI,CAAA;YACjB,KAAK,MAAM,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC,IAAI,CACjD,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAC/C,CAAC,KAAc,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAChD,CAAA;QACH,CAAC,CAAA;QACD,OAAO,CAAC,IAAI,CACV,CAAC,MAAM,EAAE,EAAE;YACT,IAAI,MAAM,CAAC,OAAO;gBAAE,OAAO,EAAE,CAAA;;gBACxB,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;QACpC,CAAC,EACD,CAAC,KAAc,EAAE,EAAE;YACjB,IAAI,MAAM,CAAC,OAAO;gBAAE,OAAO,EAAE,CAAA;;gBACxB,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;QAClC,CAAC,CACF,CAAA;QACD,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAC,IAAI,EAAE,IAAI,EAAC,CAAC,CAAA;QACvD,mEAAmE;QACnE,IAAI,MAAM,CAAC,OAAO;YAAE,OAAO,EAAE,CAAA;IAC/B,CAAC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["export interface NativeHttpResult {\n status: number\n statusText: string\n headers: Record<string, string>\n body: string\n}\n\nexport interface NativeHttpTransport {\n nativeHttpRequest(\n requestId: string,\n method: string,\n url: string,\n headers: Record<string, string>,\n body: string | null,\n ): Promise<NativeHttpResult>\n cancelNativeHttpRequest(requestId: string): Promise<void>\n}\n\nlet requestSequence = 0\n\nfunction abortReason(signal: AbortSignal): unknown {\n if (signal.reason !== undefined) return signal.reason\n const error = new Error(\"Native HTTP request aborted\")\n error.name = \"AbortError\"\n return error\n}\n\n/** Keep Android's background-safe HTTP path while cancelling its actual native request. */\nexport function nativeHttpRequest(\n native: NativeHttpTransport,\n method: string,\n url: string,\n headers: Record<string, string>,\n body: string | null,\n signal?: AbortSignal,\n): Promise<NativeHttpResult> {\n if (signal?.aborted) return Promise.reject(abortReason(signal))\n const requestId = `http-${Date.now().toString(36)}-${++requestSequence}`\n // Start first, then subscribe: Expo delivers start and cancel on the same serial queue.\n const request = native.nativeHttpRequest(requestId, method, url, headers, body)\n if (!signal) return request\n\n return new Promise((resolve, reject) => {\n let settled = false\n let cancelling = false\n const finish = (action: () => void) => {\n if (settled) return\n settled = true\n signal.removeEventListener(\"abort\", onAbort)\n action()\n }\n const onAbort = () => {\n if (settled || cancelling) return\n cancelling = true\n void native.cancelNativeHttpRequest(requestId).then(\n () => finish(() => reject(abortReason(signal))),\n (error: unknown) => finish(() => reject(error)),\n )\n }\n request.then(\n (result) => {\n if (signal.aborted) onAbort()\n else finish(() => resolve(result))\n },\n (error: unknown) => {\n if (signal.aborted) onAbort()\n else finish(() => reject(error))\n },\n )\n signal.addEventListener(\"abort\", onAbort, {once: true})\n // Handles an abort fired synchronously during native registration.\n if (signal.aborted) onAbort()\n })\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nativeHttpRequest.test.d.ts","sourceRoot":"","sources":["../src/nativeHttpRequest.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { nativeHttpRequest } from "./nativeHttpRequest";
|
|
2
|
+
const response = { status: 200, statusText: "OK", headers: {}, body: "{}" };
|
|
3
|
+
function deferred() {
|
|
4
|
+
let resolve;
|
|
5
|
+
let reject;
|
|
6
|
+
const promise = new Promise((yes, no) => {
|
|
7
|
+
resolve = yes;
|
|
8
|
+
reject = no;
|
|
9
|
+
});
|
|
10
|
+
return { promise, resolve, reject };
|
|
11
|
+
}
|
|
12
|
+
function transport(request = deferred()) {
|
|
13
|
+
const native = {
|
|
14
|
+
nativeHttpRequest: jest.fn(() => request.promise),
|
|
15
|
+
cancelNativeHttpRequest: jest.fn(async () => { }),
|
|
16
|
+
};
|
|
17
|
+
return { native, request };
|
|
18
|
+
}
|
|
19
|
+
describe("nativeHttpRequest", () => {
|
|
20
|
+
test("pre-aborted requests do not start or cancel native work", async () => {
|
|
21
|
+
const { native } = transport();
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const reason = new Error("deadline");
|
|
24
|
+
controller.abort(reason);
|
|
25
|
+
await expect(nativeHttpRequest(native, "GET", "https://example.com", {}, null, controller.signal)).rejects.toBe(reason);
|
|
26
|
+
expect(native.nativeHttpRequest).not.toHaveBeenCalled();
|
|
27
|
+
expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled();
|
|
28
|
+
});
|
|
29
|
+
test("abort cancels only this request and waits for native cancellation acceptance", async () => {
|
|
30
|
+
const { native, request } = transport();
|
|
31
|
+
const cancellation = deferred();
|
|
32
|
+
native.cancelNativeHttpRequest = jest.fn(() => cancellation.promise);
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const remove = jest.spyOn(controller.signal, "removeEventListener");
|
|
35
|
+
const promise = nativeHttpRequest(native, "POST", "https://example.com", { test: "header" }, "body", controller.signal);
|
|
36
|
+
const rejected = expect(promise).rejects.toMatchObject({ name: "AbortError" });
|
|
37
|
+
controller.abort();
|
|
38
|
+
const id = native.nativeHttpRequest.mock.calls[0][0];
|
|
39
|
+
expect(native.cancelNativeHttpRequest).toHaveBeenCalledWith(id);
|
|
40
|
+
expect(remove).not.toHaveBeenCalled();
|
|
41
|
+
request.reject(new Error("cancelled socket"));
|
|
42
|
+
cancellation.resolve();
|
|
43
|
+
await rejected;
|
|
44
|
+
expect(native.cancelNativeHttpRequest).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(remove).toHaveBeenCalledWith("abort", expect.any(Function));
|
|
46
|
+
});
|
|
47
|
+
test("an abort during native start is cancelled after start and removes its listener", async () => {
|
|
48
|
+
const { native } = transport();
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const order = [];
|
|
51
|
+
native.nativeHttpRequest = jest.fn(() => {
|
|
52
|
+
order.push("start");
|
|
53
|
+
controller.abort();
|
|
54
|
+
return new Promise(() => { });
|
|
55
|
+
});
|
|
56
|
+
native.cancelNativeHttpRequest = jest.fn(async () => { order.push("cancel"); });
|
|
57
|
+
await expect(nativeHttpRequest(native, "GET", "https://example.com", {}, null, controller.signal)).rejects.toMatchObject({ name: "AbortError" });
|
|
58
|
+
expect(order).toEqual(["start", "cancel"]);
|
|
59
|
+
});
|
|
60
|
+
test.each(["success", "failure"])("%s removes its abort listener and a later abort does not cancel", async (outcome) => {
|
|
61
|
+
const { native, request } = transport();
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const remove = jest.spyOn(controller.signal, "removeEventListener");
|
|
64
|
+
const promise = nativeHttpRequest(native, "GET", "https://example.com", {}, null, controller.signal);
|
|
65
|
+
if (outcome === "success") {
|
|
66
|
+
request.resolve(response);
|
|
67
|
+
await expect(promise).resolves.toBe(response);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
request.reject(new Error("offline"));
|
|
71
|
+
await expect(promise).rejects.toThrow("offline");
|
|
72
|
+
}
|
|
73
|
+
controller.abort();
|
|
74
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
75
|
+
expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
test("independent calls have separate IDs and unsignalled calls keep the native path", async () => {
|
|
78
|
+
const { native, request } = transport();
|
|
79
|
+
const first = nativeHttpRequest(native, "GET", "https://example.com/first", {}, null);
|
|
80
|
+
const second = nativeHttpRequest(native, "GET", "https://example.com/second", {}, null);
|
|
81
|
+
const calls = native.nativeHttpRequest.mock.calls;
|
|
82
|
+
expect(calls[0][0]).not.toBe(calls[1][0]);
|
|
83
|
+
request.resolve(response);
|
|
84
|
+
await Promise.all([first, second]);
|
|
85
|
+
expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
//# sourceMappingURL=nativeHttpRequest.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nativeHttpRequest.test.js","sourceRoot":"","sources":["../src/nativeHttpRequest.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,iBAAiB,EAAkD,MAAM,qBAAqB,CAAA;AAEtG,MAAM,QAAQ,GAAqB,EAAC,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAC,CAAA;AAE3F,SAAS,QAAQ;IACf,IAAI,OAA4B,CAAA;IAChC,IAAI,MAAiC,CAAA;IACrC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAI,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE;QACzC,OAAO,GAAG,GAAG,CAAA;QACb,MAAM,GAAG,EAAE,CAAA;IACb,CAAC,CAAC,CAAA;IACF,OAAO,EAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAC,CAAA;AACnC,CAAC;AAED,SAAS,SAAS,CAAC,UAAU,QAAQ,EAAoB;IACvD,MAAM,MAAM,GAAwB;QAClC,iBAAiB,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;QACjD,uBAAuB,EAAE,IAAI,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,GAAE,CAAC,CAAC;KACjD,CAAA;IACD,OAAO,EAAC,MAAM,EAAE,OAAO,EAAC,CAAA;AAC1B,CAAC;AAED,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,IAAI,CAAC,yDAAyD,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,EAAC,MAAM,EAAC,GAAG,SAAS,EAAE,CAAA;QAC5B,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QACxC,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,UAAU,CAAC,CAAA;QACpC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACxB,MAAM,MAAM,CAAC,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACvH,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAA;QACvD,MAAM,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAA;IAC/D,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,8EAA8E,EAAE,KAAK,IAAI,EAAE;QAC9F,MAAM,EAAC,MAAM,EAAE,OAAO,EAAC,GAAG,SAAS,EAAE,CAAA;QACrC,MAAM,YAAY,GAAG,QAAQ,EAAQ,CAAA;QACrC,MAAM,CAAC,uBAAuB,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;QACpE,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAA;QACnE,MAAM,OAAO,GAAG,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,qBAAqB,EAAE,EAAC,IAAI,EAAE,QAAQ,EAAC,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,CAAA;QACrH,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,EAAC,IAAI,EAAE,YAAY,EAAC,CAAC,CAAA;QAC5E,UAAU,CAAC,KAAK,EAAE,CAAA;QAClB,MAAM,EAAE,GAAI,MAAM,CAAC,iBAA+B,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACnE,MAAM,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAA;QAC/D,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAA;QACrC,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC,CAAA;QAC7C,YAAY,CAAC,OAAO,EAAE,CAAA;QACtB,MAAM,QAAQ,CAAA;QACd,MAAM,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAA;QAC/D,MAAM,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,gFAAgF,EAAE,KAAK,IAAI,EAAE;QAChG,MAAM,EAAC,MAAM,EAAC,GAAG,SAAS,EAAE,CAAA;QAC5B,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QACxC,MAAM,KAAK,GAAa,EAAE,CAAA;QAC1B,MAAM,CAAC,iBAAiB,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE;YACtC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;YACnB,UAAU,CAAC,KAAK,EAAE,CAAA;YAClB,OAAO,IAAI,OAAO,CAAmB,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QAChD,CAAC,CAAC,CAAA;QACF,MAAM,CAAC,uBAAuB,GAAG,IAAI,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,GAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA,CAAA,CAAC,CAAC,CAAA;QAC5E,MAAM,MAAM,CAAC,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,EAAC,IAAI,EAAE,YAAY,EAAC,CAAC,CAAA;QAC9I,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,iEAAiE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QACrH,MAAM,EAAC,MAAM,EAAE,OAAO,EAAC,GAAG,SAAS,EAAE,CAAA;QACrC,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAA;QACnE,MAAM,OAAO,GAAG,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAA;QACpG,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;YACzB,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAC/C,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,CAAA;YACpC,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;QAClD,CAAC;QACD,UAAU,CAAC,KAAK,EAAE,CAAA;QAClB,MAAM,CAAC,MAAM,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAA;QACvC,MAAM,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAA;IAC/D,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,gFAAgF,EAAE,KAAK,IAAI,EAAE;QAChG,MAAM,EAAC,MAAM,EAAE,OAAO,EAAC,GAAG,SAAS,EAAE,CAAA;QACrC,MAAM,KAAK,GAAG,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,2BAA2B,EAAE,EAAE,EAAE,IAAI,CAAC,CAAA;QACrF,MAAM,MAAM,GAAG,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,4BAA4B,EAAE,EAAE,EAAE,IAAI,CAAC,CAAA;QACvF,MAAM,KAAK,GAAI,MAAM,CAAC,iBAA+B,CAAC,IAAI,CAAC,KAAK,CAAA;QAChE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACzC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;QACzB,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;QAClC,MAAM,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAA;IAC/D,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import {nativeHttpRequest, type NativeHttpResult, type NativeHttpTransport} from \"./nativeHttpRequest\"\n\nconst response: NativeHttpResult = {status: 200, statusText: \"OK\", headers: {}, body: \"{}\"}\n\nfunction deferred<T>() {\n let resolve!: (value: T) => void\n let reject!: (error: unknown) => void\n const promise = new Promise<T>((yes, no) => {\n resolve = yes\n reject = no\n })\n return {promise, resolve, reject}\n}\n\nfunction transport(request = deferred<NativeHttpResult>()) {\n const native: NativeHttpTransport = {\n nativeHttpRequest: jest.fn(() => request.promise),\n cancelNativeHttpRequest: jest.fn(async () => {}),\n }\n return {native, request}\n}\n\ndescribe(\"nativeHttpRequest\", () => {\n test(\"pre-aborted requests do not start or cancel native work\", async () => {\n const {native} = transport()\n const controller = new AbortController()\n const reason = new Error(\"deadline\")\n controller.abort(reason)\n await expect(nativeHttpRequest(native, \"GET\", \"https://example.com\", {}, null, controller.signal)).rejects.toBe(reason)\n expect(native.nativeHttpRequest).not.toHaveBeenCalled()\n expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled()\n })\n\n test(\"abort cancels only this request and waits for native cancellation acceptance\", async () => {\n const {native, request} = transport()\n const cancellation = deferred<void>()\n native.cancelNativeHttpRequest = jest.fn(() => cancellation.promise)\n const controller = new AbortController()\n const remove = jest.spyOn(controller.signal, \"removeEventListener\")\n const promise = nativeHttpRequest(native, \"POST\", \"https://example.com\", {test: \"header\"}, \"body\", controller.signal)\n const rejected = expect(promise).rejects.toMatchObject({name: \"AbortError\"})\n controller.abort()\n const id = (native.nativeHttpRequest as jest.Mock).mock.calls[0][0]\n expect(native.cancelNativeHttpRequest).toHaveBeenCalledWith(id)\n expect(remove).not.toHaveBeenCalled()\n request.reject(new Error(\"cancelled socket\"))\n cancellation.resolve()\n await rejected\n expect(native.cancelNativeHttpRequest).toHaveBeenCalledTimes(1)\n expect(remove).toHaveBeenCalledWith(\"abort\", expect.any(Function))\n })\n\n test(\"an abort during native start is cancelled after start and removes its listener\", async () => {\n const {native} = transport()\n const controller = new AbortController()\n const order: string[] = []\n native.nativeHttpRequest = jest.fn(() => {\n order.push(\"start\")\n controller.abort()\n return new Promise<NativeHttpResult>(() => {})\n })\n native.cancelNativeHttpRequest = jest.fn(async () => {order.push(\"cancel\")})\n await expect(nativeHttpRequest(native, \"GET\", \"https://example.com\", {}, null, controller.signal)).rejects.toMatchObject({name: \"AbortError\"})\n expect(order).toEqual([\"start\", \"cancel\"])\n })\n\n test.each([\"success\", \"failure\"])(\"%s removes its abort listener and a later abort does not cancel\", async (outcome) => {\n const {native, request} = transport()\n const controller = new AbortController()\n const remove = jest.spyOn(controller.signal, \"removeEventListener\")\n const promise = nativeHttpRequest(native, \"GET\", \"https://example.com\", {}, null, controller.signal)\n if (outcome === \"success\") {\n request.resolve(response)\n await expect(promise).resolves.toBe(response)\n } else {\n request.reject(new Error(\"offline\"))\n await expect(promise).rejects.toThrow(\"offline\")\n }\n controller.abort()\n expect(remove).toHaveBeenCalledTimes(1)\n expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled()\n })\n\n test(\"independent calls have separate IDs and unsignalled calls keep the native path\", async () => {\n const {native, request} = transport()\n const first = nativeHttpRequest(native, \"GET\", \"https://example.com/first\", {}, null)\n const second = nativeHttpRequest(native, \"GET\", \"https://example.com/second\", {}, null)\n const calls = (native.nativeHttpRequest as jest.Mock).mock.calls\n expect(calls[0][0]).not.toBe(calls[1][0])\n request.resolve(response)\n await Promise.all([first, second])\n expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled()\n })\n})\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/crust",
|
|
3
|
-
"version": "3.3.0-dev.
|
|
3
|
+
"version": "3.3.0-dev.780",
|
|
4
4
|
"description": "Mentra Native Module",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"license": "Apache-2.0",
|
|
36
36
|
"homepage": "https://github.com/Mentra-Community/MentraOS/tree/dev/mobile/modules/crust#readme",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@mentra/jspolyfill": "3.3.0-dev.
|
|
38
|
+
"@mentra/jspolyfill": "3.3.0-dev.780"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@expo/config-plugins": ">=8.0.0",
|
package/src/CrustModule.ts
CHANGED
|
@@ -11,11 +11,13 @@ declare class CrustModule extends NativeModule<CrustModuleEvents> {
|
|
|
11
11
|
/** Android: copy the displayed view's window pixels to a temporary JPEG URI. */
|
|
12
12
|
captureMiniappPreview(viewTag: number): Promise<string>
|
|
13
13
|
nativeHttpRequest(
|
|
14
|
+
requestId: string,
|
|
14
15
|
method: string,
|
|
15
16
|
url: string,
|
|
16
17
|
headers: Record<string, string>,
|
|
17
18
|
body?: string | null,
|
|
18
19
|
): Promise<{status: number; statusText: string; headers: Record<string, string>; body: string}>
|
|
20
|
+
cancelNativeHttpRequest(requestId: string): Promise<void>
|
|
19
21
|
showAVRoutePicker(tintColor?: string | null): void
|
|
20
22
|
|
|
21
23
|
/**
|
package/src/CrustModule.web.ts
CHANGED
|
@@ -4,18 +4,29 @@ import {CrustModuleEvents} from "./Crust.types"
|
|
|
4
4
|
|
|
5
5
|
class CrustModule extends NativeModule<CrustModuleEvents> {
|
|
6
6
|
PI = Math.PI
|
|
7
|
+
private readonly httpRequests = new Map<string, AbortController>()
|
|
7
8
|
async setValueAsync(value: string): Promise<void> {
|
|
8
9
|
this.emit("onChange", {value})
|
|
9
10
|
}
|
|
10
|
-
async nativeHttpRequest(method: string, url: string, headers: Record<string, string>, body?: string | null) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
async nativeHttpRequest(requestId: string, method: string, url: string, headers: Record<string, string>, body?: string | null) {
|
|
12
|
+
const controller = new AbortController()
|
|
13
|
+
this.httpRequests.set(requestId, controller)
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(url, {method, headers, body, signal: controller.signal})
|
|
16
|
+
return {
|
|
17
|
+
status: response.status,
|
|
18
|
+
statusText: response.statusText,
|
|
19
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
20
|
+
body: await response.text(),
|
|
21
|
+
}
|
|
22
|
+
} finally {
|
|
23
|
+
this.httpRequests.delete(requestId)
|
|
17
24
|
}
|
|
18
25
|
}
|
|
26
|
+
async cancelNativeHttpRequest(requestId: string): Promise<void> {
|
|
27
|
+
this.httpRequests.get(requestId)?.abort()
|
|
28
|
+
this.httpRequests.delete(requestId)
|
|
29
|
+
}
|
|
19
30
|
hello() {
|
|
20
31
|
return "Hello world! 👋"
|
|
21
32
|
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {nativeHttpRequest, type NativeHttpResult, type NativeHttpTransport} from "./nativeHttpRequest"
|
|
2
|
+
|
|
3
|
+
const response: NativeHttpResult = {status: 200, statusText: "OK", headers: {}, body: "{}"}
|
|
4
|
+
|
|
5
|
+
function deferred<T>() {
|
|
6
|
+
let resolve!: (value: T) => void
|
|
7
|
+
let reject!: (error: unknown) => void
|
|
8
|
+
const promise = new Promise<T>((yes, no) => {
|
|
9
|
+
resolve = yes
|
|
10
|
+
reject = no
|
|
11
|
+
})
|
|
12
|
+
return {promise, resolve, reject}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function transport(request = deferred<NativeHttpResult>()) {
|
|
16
|
+
const native: NativeHttpTransport = {
|
|
17
|
+
nativeHttpRequest: jest.fn(() => request.promise),
|
|
18
|
+
cancelNativeHttpRequest: jest.fn(async () => {}),
|
|
19
|
+
}
|
|
20
|
+
return {native, request}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("nativeHttpRequest", () => {
|
|
24
|
+
test("pre-aborted requests do not start or cancel native work", async () => {
|
|
25
|
+
const {native} = transport()
|
|
26
|
+
const controller = new AbortController()
|
|
27
|
+
const reason = new Error("deadline")
|
|
28
|
+
controller.abort(reason)
|
|
29
|
+
await expect(nativeHttpRequest(native, "GET", "https://example.com", {}, null, controller.signal)).rejects.toBe(reason)
|
|
30
|
+
expect(native.nativeHttpRequest).not.toHaveBeenCalled()
|
|
31
|
+
expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test("abort cancels only this request and waits for native cancellation acceptance", async () => {
|
|
35
|
+
const {native, request} = transport()
|
|
36
|
+
const cancellation = deferred<void>()
|
|
37
|
+
native.cancelNativeHttpRequest = jest.fn(() => cancellation.promise)
|
|
38
|
+
const controller = new AbortController()
|
|
39
|
+
const remove = jest.spyOn(controller.signal, "removeEventListener")
|
|
40
|
+
const promise = nativeHttpRequest(native, "POST", "https://example.com", {test: "header"}, "body", controller.signal)
|
|
41
|
+
const rejected = expect(promise).rejects.toMatchObject({name: "AbortError"})
|
|
42
|
+
controller.abort()
|
|
43
|
+
const id = (native.nativeHttpRequest as jest.Mock).mock.calls[0][0]
|
|
44
|
+
expect(native.cancelNativeHttpRequest).toHaveBeenCalledWith(id)
|
|
45
|
+
expect(remove).not.toHaveBeenCalled()
|
|
46
|
+
request.reject(new Error("cancelled socket"))
|
|
47
|
+
cancellation.resolve()
|
|
48
|
+
await rejected
|
|
49
|
+
expect(native.cancelNativeHttpRequest).toHaveBeenCalledTimes(1)
|
|
50
|
+
expect(remove).toHaveBeenCalledWith("abort", expect.any(Function))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("an abort during native start is cancelled after start and removes its listener", async () => {
|
|
54
|
+
const {native} = transport()
|
|
55
|
+
const controller = new AbortController()
|
|
56
|
+
const order: string[] = []
|
|
57
|
+
native.nativeHttpRequest = jest.fn(() => {
|
|
58
|
+
order.push("start")
|
|
59
|
+
controller.abort()
|
|
60
|
+
return new Promise<NativeHttpResult>(() => {})
|
|
61
|
+
})
|
|
62
|
+
native.cancelNativeHttpRequest = jest.fn(async () => {order.push("cancel")})
|
|
63
|
+
await expect(nativeHttpRequest(native, "GET", "https://example.com", {}, null, controller.signal)).rejects.toMatchObject({name: "AbortError"})
|
|
64
|
+
expect(order).toEqual(["start", "cancel"])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test.each(["success", "failure"])("%s removes its abort listener and a later abort does not cancel", async (outcome) => {
|
|
68
|
+
const {native, request} = transport()
|
|
69
|
+
const controller = new AbortController()
|
|
70
|
+
const remove = jest.spyOn(controller.signal, "removeEventListener")
|
|
71
|
+
const promise = nativeHttpRequest(native, "GET", "https://example.com", {}, null, controller.signal)
|
|
72
|
+
if (outcome === "success") {
|
|
73
|
+
request.resolve(response)
|
|
74
|
+
await expect(promise).resolves.toBe(response)
|
|
75
|
+
} else {
|
|
76
|
+
request.reject(new Error("offline"))
|
|
77
|
+
await expect(promise).rejects.toThrow("offline")
|
|
78
|
+
}
|
|
79
|
+
controller.abort()
|
|
80
|
+
expect(remove).toHaveBeenCalledTimes(1)
|
|
81
|
+
expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test("independent calls have separate IDs and unsignalled calls keep the native path", async () => {
|
|
85
|
+
const {native, request} = transport()
|
|
86
|
+
const first = nativeHttpRequest(native, "GET", "https://example.com/first", {}, null)
|
|
87
|
+
const second = nativeHttpRequest(native, "GET", "https://example.com/second", {}, null)
|
|
88
|
+
const calls = (native.nativeHttpRequest as jest.Mock).mock.calls
|
|
89
|
+
expect(calls[0][0]).not.toBe(calls[1][0])
|
|
90
|
+
request.resolve(response)
|
|
91
|
+
await Promise.all([first, second])
|
|
92
|
+
expect(native.cancelNativeHttpRequest).not.toHaveBeenCalled()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export interface NativeHttpResult {
|
|
2
|
+
status: number
|
|
3
|
+
statusText: string
|
|
4
|
+
headers: Record<string, string>
|
|
5
|
+
body: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface NativeHttpTransport {
|
|
9
|
+
nativeHttpRequest(
|
|
10
|
+
requestId: string,
|
|
11
|
+
method: string,
|
|
12
|
+
url: string,
|
|
13
|
+
headers: Record<string, string>,
|
|
14
|
+
body: string | null,
|
|
15
|
+
): Promise<NativeHttpResult>
|
|
16
|
+
cancelNativeHttpRequest(requestId: string): Promise<void>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let requestSequence = 0
|
|
20
|
+
|
|
21
|
+
function abortReason(signal: AbortSignal): unknown {
|
|
22
|
+
if (signal.reason !== undefined) return signal.reason
|
|
23
|
+
const error = new Error("Native HTTP request aborted")
|
|
24
|
+
error.name = "AbortError"
|
|
25
|
+
return error
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Keep Android's background-safe HTTP path while cancelling its actual native request. */
|
|
29
|
+
export function nativeHttpRequest(
|
|
30
|
+
native: NativeHttpTransport,
|
|
31
|
+
method: string,
|
|
32
|
+
url: string,
|
|
33
|
+
headers: Record<string, string>,
|
|
34
|
+
body: string | null,
|
|
35
|
+
signal?: AbortSignal,
|
|
36
|
+
): Promise<NativeHttpResult> {
|
|
37
|
+
if (signal?.aborted) return Promise.reject(abortReason(signal))
|
|
38
|
+
const requestId = `http-${Date.now().toString(36)}-${++requestSequence}`
|
|
39
|
+
// Start first, then subscribe: Expo delivers start and cancel on the same serial queue.
|
|
40
|
+
const request = native.nativeHttpRequest(requestId, method, url, headers, body)
|
|
41
|
+
if (!signal) return request
|
|
42
|
+
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
let settled = false
|
|
45
|
+
let cancelling = false
|
|
46
|
+
const finish = (action: () => void) => {
|
|
47
|
+
if (settled) return
|
|
48
|
+
settled = true
|
|
49
|
+
signal.removeEventListener("abort", onAbort)
|
|
50
|
+
action()
|
|
51
|
+
}
|
|
52
|
+
const onAbort = () => {
|
|
53
|
+
if (settled || cancelling) return
|
|
54
|
+
cancelling = true
|
|
55
|
+
void native.cancelNativeHttpRequest(requestId).then(
|
|
56
|
+
() => finish(() => reject(abortReason(signal))),
|
|
57
|
+
(error: unknown) => finish(() => reject(error)),
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
request.then(
|
|
61
|
+
(result) => {
|
|
62
|
+
if (signal.aborted) onAbort()
|
|
63
|
+
else finish(() => resolve(result))
|
|
64
|
+
},
|
|
65
|
+
(error: unknown) => {
|
|
66
|
+
if (signal.aborted) onAbort()
|
|
67
|
+
else finish(() => reject(error))
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
signal.addEventListener("abort", onAbort, {once: true})
|
|
71
|
+
// Handles an abort fired synchronously during native registration.
|
|
72
|
+
if (signal.aborted) onAbort()
|
|
73
|
+
})
|
|
74
|
+
}
|