@mentra/crust 3.5.0-dev.1239 → 3.5.0-dev.1242

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.
@@ -262,9 +262,11 @@ class JSCRuntime private constructor(private val appContext: Context) {
262
262
  // anyway, so this is safe.
263
263
  val qjs = try {
264
264
  executor.submit<QuickJs> {
265
- QuickJs.create(Dispatchers.Unconfined).apply {
266
- memoryLimit = 32L * 1024 * 1024 // 32 MB heap per miniapp
267
- maxStackSize = 512L * 1024 // 512 KB stack
265
+ QuickJsNativeLifecycle.withNative {
266
+ QuickJs.create(Dispatchers.Unconfined).apply {
267
+ memoryLimit = 32L * 1024 * 1024 // 32 MB heap per miniapp
268
+ maxStackSize = 512L * 1024 // 512 KB stack
269
+ }
268
270
  }
269
271
  }.get()
270
272
  } catch (e: Throwable) {
@@ -575,7 +577,7 @@ class JSCRuntime private constructor(private val appContext: Context) {
575
577
  try {
576
578
  record.executor.submit {
577
579
  try {
578
- record.qjs.close()
580
+ QuickJsNativeLifecycle.withNative { record.qjs.close() }
579
581
  } catch (e: Throwable) {
580
582
  Log.w(TAG, "QuickJs.close() threw for $packageName: ${e.message}")
581
583
  }
@@ -0,0 +1,16 @@
1
+ package com.mentra.crust.jsc
2
+
3
+ /**
4
+ * quickjs-kt 1.0.15 keeps one process-wide JavaVM and an instance counter.
5
+ * Neither update is atomic. Two miniapps creating or closing at the same time
6
+ * can drop that VM while another context is still open, and the next call
7
+ * logs "Cannot get jni env because the vm is not cached".
8
+ *
9
+ * Every [com.dokar.quickjs.QuickJs.create] and [com.dokar.quickjs.QuickJs.close]
10
+ * in this process goes through [withNative]. Evaluation stays outside the lock.
11
+ */
12
+ internal object QuickJsNativeLifecycle {
13
+ private val lock = Any()
14
+
15
+ fun <T> withNative(block: () -> T): T = synchronized(lock) { block() }
16
+ }
@@ -3,7 +3,6 @@ package com.mentra.crust.jsc
3
3
  import com.dokar.quickjs.QuickJs
4
4
  import com.dokar.quickjs.QuickJsException
5
5
  import com.dokar.quickjs.binding.function
6
- import java.util.concurrent.Executors
7
6
  import kotlinx.coroutines.Dispatchers
8
7
  import kotlinx.coroutines.runBlocking
9
8
  import kotlinx.coroutines.withTimeout
@@ -16,7 +15,11 @@ import org.junit.Test
16
15
  * bindings called from JS, and independent close orders.
17
16
  */
18
17
  class QuickJsLifecycleTest {
19
- private fun create() = QuickJs.create(Dispatchers.Unconfined)
18
+ private fun create() = QuickJsNativeLifecycle.withNative {
19
+ QuickJs.create(Dispatchers.Unconfined)
20
+ }
21
+
22
+ private fun QuickJs.release() = QuickJsNativeLifecycle.withNative { close() }
20
23
 
21
24
  @Test fun `closing one instance keeps another instance's callbacks working`() = runBlocking {
22
25
  var calls = 0
@@ -24,31 +27,31 @@ class QuickJsLifecycleTest {
24
27
  survivor.function("hostCallback") { calls++; 42 }
25
28
  val other = create()
26
29
  assertEquals(42, survivor.evaluate<Int>("hostCallback()"))
27
- other.close()
30
+ other.release()
28
31
  withTimeout(1000) { assertEquals(42, survivor.evaluate<Int>("hostCallback()")) }
29
32
  assertEquals(2, calls)
30
33
  repeat(20) {
31
- create().close()
34
+ create().release()
32
35
  assertEquals(42, survivor.evaluate<Int>("hostCallback()"))
33
36
  }
34
- val workers = Executors.newFixedThreadPool(4)
37
+ val workers = java.util.concurrent.Executors.newFixedThreadPool(4)
35
38
  try {
36
- (1..40).map { workers.submit { create().close() } }.forEach { it.get() }
39
+ (1..40).map { workers.submit { create().release() } }.forEach { it.get() }
37
40
  assertEquals(42, survivor.evaluate<Int>("hostCallback()"))
38
41
  } finally {
39
42
  workers.shutdownNow()
40
43
  }
41
- survivor.close()
44
+ survivor.release()
42
45
  val fresh = create()
43
46
  fresh.function("hostCallback") { 7 }
44
47
  assertEquals(7, fresh.evaluate<Int>("hostCallback()"))
45
- fresh.close()
48
+ fresh.release()
46
49
  val older = create()
47
50
  val newer = create()
48
51
  newer.function("hostCallback") { 9 }
49
- older.close()
52
+ older.release()
50
53
  assertEquals(9, newer.evaluate<Int>("hostCallback()"))
51
- newer.close()
54
+ newer.release()
52
55
  }
53
56
 
54
57
  @Test fun `a binding exception is a catchable JS Error carrying the message`() = runBlocking {
@@ -58,7 +61,7 @@ class QuickJsLifecycleTest {
58
61
  "try { __dispatch(); 'no throw' } catch (e) { (e instanceof Error) + '|' + e.message }",
59
62
  )
60
63
  assertEquals("true|INVALID_ARGS: iface", caught)
61
- qjs.close()
64
+ qjs.release()
62
65
  }
63
66
 
64
67
  @Test fun `non-ASCII strings cross the bridge unchanged`() = runBlocking {
@@ -69,7 +72,7 @@ class QuickJsLifecycleTest {
69
72
  qjs.function("hostEcho") { args -> received = args[0] as String; args[0] }
70
73
  assertEquals(text, qjs.evaluate<String>("hostEcho(hostText())"))
71
74
  assertEquals(text, received)
72
- qjs.close()
75
+ qjs.release()
73
76
  }
74
77
 
75
78
  @Test fun `unusual thrown values fail as exceptions, not crashes`() = runBlocking {
@@ -82,6 +85,6 @@ class QuickJsLifecycleTest {
82
85
  }
83
86
  assertEquals(2, qjs.evaluate<Int>("1 + 1"))
84
87
  }
85
- qjs.close()
88
+ qjs.release()
86
89
  }
87
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/crust",
3
- "version": "3.5.0-dev.1239",
3
+ "version": "3.5.0-dev.1242",
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.5.0-dev.1239"
38
+ "@mentra/jspolyfill": "3.5.0-dev.1242"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@expo/config-plugins": ">=8.0.0",