quickjs 0.17.0.rc1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,7 @@ const char *featurePolyfillCryptoId = "feature_polyfill_crypto";
13
13
 
14
14
  const char *undefinedId = "undefined";
15
15
  const char *nanId = "NaN";
16
+ const char *vmInternalFilename = "<vm>";
16
17
 
17
18
  const char *native_errors[] = {
18
19
  "SyntaxError",
@@ -32,6 +33,9 @@ static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, VALUE r_visited);
32
33
  static VALUE vm_m_memoryUsage(VALUE r_self);
33
34
  static VALUE vm_m_runGC(VALUE r_self);
34
35
  static VALUE vm_m_memoryPoisoned(VALUE r_self);
36
+ static VALUE vm_m_dispose(VALUE r_self);
37
+ static VALUE vm_m_disposed(VALUE r_self);
38
+ static VALUE vm_m_drainJobs(VALUE r_self);
35
39
 
36
40
  JSValue j_error_from_ruby_error(JSContext *ctx, VALUE r_error)
37
41
  {
@@ -187,6 +191,28 @@ VALUE r_try_json_parse(VALUE r_str)
187
191
  return rb_funcall(rb_const_get(rb_cClass, rb_intern("JSON")), rb_intern("parse"), 1, r_str);
188
192
  }
189
193
 
194
+ // Convert a JS Error.stack string into a Ruby Array suitable for
195
+ // Exception#set_backtrace. Lines are stripped; empty lines (including the
196
+ // trailing newline that QuickJS appends) are dropped. The frames keep
197
+ // QuickJS's native format ("at func (file:line)") — Ruby's backtrace API
198
+ // doesn't enforce a layout, and reshaping into "file:line:in 'method'"
199
+ // would lose information for no real win.
200
+ static VALUE r_backtrace_from_js_stack(const char *stack)
201
+ {
202
+ if (stack == NULL || stack[0] == '\0')
203
+ return Qnil;
204
+
205
+ VALUE r_lines = rb_str_split(rb_str_new_cstr(stack), "\n");
206
+ VALUE r_filtered = rb_ary_new();
207
+ for (long i = 0; i < RARRAY_LEN(r_lines); i++)
208
+ {
209
+ VALUE r_line = rb_funcall(rb_ary_entry(r_lines, i), rb_intern("strip"), 0);
210
+ if (RSTRING_LEN(r_line) > 0)
211
+ rb_ary_push(r_filtered, r_line);
212
+ }
213
+ return RARRAY_LEN(r_filtered) > 0 ? r_filtered : Qnil;
214
+ }
215
+
190
216
  VALUE to_r_json(JSContext *ctx, JSValue j_val)
191
217
  {
192
218
  JSValue j_stringified = JS_JSONStringify(ctx, j_val, JS_UNDEFINED, JS_UNDEFINED);
@@ -431,14 +457,11 @@ static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, VALUE r_visited)
431
457
  VMData *data = JS_GetContextOpaque(ctx);
432
458
  VALUE r_headline = rb_str_new2(headline);
433
459
  dispatch_log(data, "error", rb_ary_new3(1, r_log_body_new(r_headline, r_headline)));
434
-
435
- JS_FreeValue(ctx, j_errorClassMessage);
436
- JS_FreeValue(ctx, j_errorClassName);
437
- JS_FreeValue(ctx, j_stackTrace);
438
- JS_FreeCString(ctx, stackTrace);
439
460
  free(headline);
440
461
 
441
462
  VALUE r_error_class, r_error_message = rb_str_new2(errorClassMessage);
463
+ VALUE r_error_name = rb_str_new2(errorClassName);
464
+ VALUE r_backtrace = r_backtrace_from_js_stack(stackTrace);
442
465
  if (is_native_error_name(errorClassName))
443
466
  {
444
467
  r_error_class = QUICKJSRB_ERROR_FOR(errorClassName);
@@ -464,11 +487,18 @@ static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, VALUE r_visited)
464
487
  {
465
488
  r_error_class = QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR);
466
489
  }
490
+ JS_FreeValue(ctx, j_errorClassMessage);
491
+ JS_FreeValue(ctx, j_errorClassName);
492
+ JS_FreeValue(ctx, j_stackTrace);
493
+ JS_FreeCString(ctx, stackTrace);
467
494
  JS_FreeCString(ctx, errorClassName);
468
495
  JS_FreeCString(ctx, errorClassMessage);
469
496
  JS_FreeValue(ctx, j_exceptionVal);
470
497
 
471
- rb_exc_raise(rb_funcall(r_error_class, rb_intern("new"), 2, r_error_message, rb_str_new2(errorClassName)));
498
+ VALUE r_exc = rb_funcall(r_error_class, rb_intern("new"), 2, r_error_message, r_error_name);
499
+ if (!NIL_P(r_backtrace))
500
+ rb_funcall(r_exc, rb_intern("set_backtrace"), 1, r_backtrace);
501
+ rb_exc_raise(r_exc);
472
502
  }
473
503
  else // exception without Error object
474
504
  {
@@ -514,24 +544,48 @@ static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, VALUE r_visited)
514
544
  struct module_loader_call_args
515
545
  {
516
546
  VALUE proc;
517
- VALUE r_module_name;
547
+ VALUE r_specifier;
548
+ VALUE r_importer;
518
549
  };
519
550
 
551
+ // Calls the user's loader with one or two args based on its arity. Procs
552
+ // declared with a single positional (`->(name) { ... }`) get the legacy
553
+ // 1-arg form so existing callers keep working; everything else (2-arity
554
+ // lambdas, varargs procs) receives `(specifier, importer)`.
520
555
  static VALUE r_module_loader_call(VALUE r_args_val)
521
556
  {
522
557
  struct module_loader_call_args *args = (struct module_loader_call_args *)r_args_val;
523
- return rb_funcall(args->proc, rb_intern("call"), 1, args->r_module_name);
558
+ int arity = NUM2INT(rb_funcall(args->proc, rb_intern("arity"), 0));
559
+ if (arity == 1)
560
+ return rb_funcall(args->proc, rb_intern("call"), 1, args->r_specifier);
561
+ return rb_funcall(args->proc, rb_intern("call"), 2, args->r_specifier, args->r_importer);
524
562
  }
525
563
 
526
- static JSModuleDef *quickjsrb_module_loader(JSContext *ctx, const char *module_name, void *opaque, JSValueConst attributes)
564
+ // Normalize hook. Resolves `(specifier, importer)` to a canonical name by
565
+ // consulting the resolution cache or invoking the user's loader Proc.
566
+ // The Proc's return value drives both the canonical name AND the source
567
+ // the load hook will eval:
568
+ // - String → canonical = specifier, source = the string
569
+ // - { code:, as: } → canonical = as, source = code
570
+ // - nil / false → ReferenceError
571
+ // Source is stashed in `module_source_cache` keyed by canonical so the
572
+ // load hook can pick it up. The resolution cache memoizes the call so
573
+ // the Proc fires at most once per `(specifier, importer)` pair.
574
+ static char *quickjsrb_module_normalize(JSContext *ctx, const char *base_name, const char *name, void *opaque)
527
575
  {
528
576
  VMData *data = JS_GetContextOpaque(ctx);
529
- if (NIL_P(data->module_loader))
530
- return js_module_loader(ctx, module_name, opaque, attributes);
531
577
 
532
- struct module_loader_call_args args = {data->module_loader, rb_str_new_cstr(module_name)};
578
+ VALUE r_specifier = rb_str_new_cstr(name);
579
+ VALUE r_importer = rb_str_new_cstr(base_name);
580
+ VALUE r_key = rb_ary_new3(2, r_specifier, r_importer);
581
+
582
+ VALUE r_cached_canonical = rb_hash_aref(data->module_resolution_cache, r_key);
583
+ if (!NIL_P(r_cached_canonical))
584
+ return js_strdup(ctx, StringValueCStr(r_cached_canonical));
585
+
586
+ struct module_loader_call_args args = {data->module_loader, r_specifier, r_importer};
533
587
  int state;
534
- VALUE r_source = rb_protect(r_module_loader_call, (VALUE)&args, &state);
588
+ VALUE r_return = rb_protect(r_module_loader_call, (VALUE)&args, &state);
535
589
  if (state)
536
590
  {
537
591
  VALUE r_error = rb_errinfo();
@@ -541,17 +595,61 @@ static JSModuleDef *quickjsrb_module_loader(JSContext *ctx, const char *module_n
541
595
  return NULL;
542
596
  }
543
597
 
544
- if (NIL_P(r_source) || r_source == Qfalse)
598
+ if (NIL_P(r_return) || r_return == Qfalse)
599
+ {
600
+ JS_ThrowReferenceError(ctx, "module loader returned no source for '%s'", name);
601
+ return NULL;
602
+ }
603
+
604
+ VALUE r_canonical, r_source;
605
+ if (RB_TYPE_P(r_return, T_STRING))
606
+ {
607
+ r_canonical = r_specifier;
608
+ r_source = r_return;
609
+ }
610
+ else if (RB_TYPE_P(r_return, T_HASH))
611
+ {
612
+ r_source = rb_hash_aref(r_return, ID2SYM(rb_intern("code")));
613
+ r_canonical = rb_hash_aref(r_return, ID2SYM(rb_intern("as")));
614
+ if (!RB_TYPE_P(r_source, T_STRING))
615
+ {
616
+ JS_ThrowTypeError(ctx, "module loader Hash must include code: (String, the module source)");
617
+ return NULL;
618
+ }
619
+ if (!RB_TYPE_P(r_canonical, T_STRING))
620
+ {
621
+ JS_ThrowTypeError(ctx, "module loader Hash must include as: (String, the canonical module name)");
622
+ return NULL;
623
+ }
624
+ }
625
+ else
545
626
  {
546
- JS_ThrowReferenceError(ctx, "module loader returned no source for '%s'", module_name);
627
+ JS_ThrowTypeError(ctx, "module loader must return a String, a Hash with code: and as:, or nil; got %s",
628
+ rb_obj_classname(r_return));
547
629
  return NULL;
548
630
  }
549
631
 
550
- if (!RB_TYPE_P(r_source, T_STRING))
632
+ rb_hash_aset(data->module_source_cache, r_canonical, r_source);
633
+ rb_hash_aset(data->module_resolution_cache, r_key, r_canonical);
634
+
635
+ return js_strdup(ctx, StringValueCStr(r_canonical));
636
+ }
637
+
638
+ static JSModuleDef *quickjsrb_module_loader(JSContext *ctx, const char *module_name, void *opaque, JSValueConst attributes)
639
+ {
640
+ VMData *data = JS_GetContextOpaque(ctx);
641
+
642
+ VALUE r_canonical = rb_str_new_cstr(module_name);
643
+ VALUE r_source = rb_hash_aref(data->module_source_cache, r_canonical);
644
+ if (NIL_P(r_source))
551
645
  {
552
- JS_ThrowTypeError(ctx, "module loader must return a String or nil, got %s", rb_obj_classname(r_source));
646
+ // Defensive: normalize populates this on every miss.
647
+ JS_ThrowReferenceError(ctx, "module loader: no cached source for '%s'", module_name);
553
648
  return NULL;
554
649
  }
650
+ // QuickJS won't call load again for this canonical — its own module cache
651
+ // takes over — so the source is dead weight once we've compiled it.
652
+ rb_hash_delete(data->module_source_cache, r_canonical);
555
653
 
556
654
  JSValue j_func = JS_Eval(ctx, RSTRING_PTR(r_source), RSTRING_LEN(r_source), module_name,
557
655
  JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
@@ -564,6 +662,95 @@ static JSModuleDef *quickjsrb_module_loader(JSContext *ctx, const char *module_n
564
662
  return m;
565
663
  }
566
664
 
665
+ // When no Ruby loader is set we hand resolution back to QuickJS's defaults
666
+ // (URL-style normalize + filesystem load). When a loader is set we own both
667
+ // phases so we can thread (specifier, importer) through and honor `as:`.
668
+ static void register_module_loader_funcs(VMData *data)
669
+ {
670
+ JSRuntime *runtime = JS_GetRuntime(data->context);
671
+ if (NIL_P(data->module_loader))
672
+ JS_SetModuleLoaderFunc2(runtime, NULL, js_module_loader, js_module_check_attributes, NULL);
673
+ else
674
+ JS_SetModuleLoaderFunc2(runtime, quickjsrb_module_normalize, quickjsrb_module_loader, js_module_check_attributes, NULL);
675
+ }
676
+
677
+ static VALUE r_exception_from_js_reason(JSContext *ctx, JSValueConst j_reason)
678
+ {
679
+ if (JS_IsError(ctx, j_reason))
680
+ {
681
+ VALUE r_maybe_ruby_error = find_ruby_error(ctx, j_reason);
682
+ if (!NIL_P(r_maybe_ruby_error))
683
+ return r_maybe_ruby_error;
684
+
685
+ JSValue j_name = JS_GetPropertyStr(ctx, j_reason, "name");
686
+ JSValue j_message = JS_GetPropertyStr(ctx, j_reason, "message");
687
+ JSValue j_stack = JS_GetPropertyStr(ctx, j_reason, "stack");
688
+ const char *name = JS_ToCString(ctx, j_name);
689
+ const char *message = JS_ToCString(ctx, j_message);
690
+ const char *stack = JS_ToCString(ctx, j_stack);
691
+
692
+ VALUE r_class = is_native_error_name(name)
693
+ ? QUICKJSRB_ERROR_FOR(name)
694
+ : QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR);
695
+ VALUE r_exc = rb_funcall(r_class, rb_intern("new"), 2,
696
+ rb_str_new2(message), rb_str_new2(name));
697
+ VALUE r_backtrace = r_backtrace_from_js_stack(stack);
698
+ if (!NIL_P(r_backtrace))
699
+ rb_funcall(r_exc, rb_intern("set_backtrace"), 1, r_backtrace);
700
+
701
+ JS_FreeCString(ctx, name);
702
+ JS_FreeCString(ctx, message);
703
+ if (stack)
704
+ JS_FreeCString(ctx, stack);
705
+ JS_FreeValue(ctx, j_name);
706
+ JS_FreeValue(ctx, j_message);
707
+ JS_FreeValue(ctx, j_stack);
708
+ return r_exc;
709
+ }
710
+
711
+ const char *str = JS_ToCString(ctx, j_reason);
712
+ VALUE r_exc = rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"),
713
+ 2, rb_str_new2(str ? str : "(non-stringifiable rejection)"), Qnil);
714
+ if (str)
715
+ JS_FreeCString(ctx, str);
716
+ return r_exc;
717
+ }
718
+
719
+ struct rejection_call_args
720
+ {
721
+ VALUE proc;
722
+ VALUE r_reason;
723
+ };
724
+
725
+ static VALUE r_rejection_call(VALUE r_args_val)
726
+ {
727
+ struct rejection_call_args *args = (struct rejection_call_args *)r_args_val;
728
+ return rb_funcall(args->proc, rb_intern("call"), 1, args->r_reason);
729
+ }
730
+
731
+ static void quickjsrb_promise_rejection_tracker(
732
+ JSContext *ctx, JSValueConst promise, JSValueConst reason,
733
+ JS_BOOL is_handled, void *opaque)
734
+ {
735
+ if (is_handled)
736
+ return;
737
+
738
+ VMData *data = JS_GetContextOpaque(ctx);
739
+ if (NIL_P(data->on_unhandled_rejection))
740
+ return;
741
+
742
+ VALUE r_reason = r_exception_from_js_reason(ctx, reason);
743
+ struct rejection_call_args args = {data->on_unhandled_rejection, r_reason};
744
+ int state;
745
+ rb_protect(r_rejection_call, (VALUE)&args, &state);
746
+ if (state)
747
+ {
748
+ // Longjmping out of a QuickJS host callback corrupts the runtime, so
749
+ // a raise inside the user's tracker has to be dropped on the floor.
750
+ rb_set_errinfo(Qnil);
751
+ }
752
+ }
753
+
567
754
  static VALUE r_try_call_proc(VALUE r_try_args)
568
755
  {
569
756
  return rb_funcall(
@@ -860,7 +1047,8 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
860
1047
  JS_SetMemoryLimit(runtime, NUM2UINT(r_memory_limit));
861
1048
  JS_SetMaxStackSize(runtime, NUM2UINT(r_max_stack_size));
862
1049
 
863
- JS_SetModuleLoaderFunc2(runtime, NULL, quickjsrb_module_loader, js_module_check_attributes, NULL);
1050
+ register_module_loader_funcs(data);
1051
+ JS_SetHostPromiseRejectionTracker(runtime, quickjsrb_promise_rejection_tracker, NULL);
864
1052
  js_std_init_handlers(runtime);
865
1053
 
866
1054
  JSValue j_global = JS_GetGlobalObject(data->context);
@@ -870,7 +1058,7 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
870
1058
  js_init_module_std(data->context, "std");
871
1059
  const char *enableStd = "import * as std from 'std';\n"
872
1060
  "globalThis.std = std;\n";
873
- JSValue j_stdEval = JS_Eval(data->context, enableStd, strlen(enableStd), "<vm>", JS_EVAL_TYPE_MODULE);
1061
+ JSValue j_stdEval = JS_Eval(data->context, enableStd, strlen(enableStd), vmInternalFilename, JS_EVAL_TYPE_MODULE);
874
1062
  JS_FreeValue(data->context, j_stdEval);
875
1063
  }
876
1064
 
@@ -879,7 +1067,7 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
879
1067
  js_init_module_os(data->context, "os");
880
1068
  const char *enableOs = "import * as os from 'os';\n"
881
1069
  "globalThis.os = os;\n";
882
- JSValue j_osEval = JS_Eval(data->context, enableOs, strlen(enableOs), "<vm>", JS_EVAL_TYPE_MODULE);
1070
+ JSValue j_osEval = JS_Eval(data->context, enableOs, strlen(enableOs), vmInternalFilename, JS_EVAL_TYPE_MODULE);
883
1071
  JS_FreeValue(data->context, j_osEval);
884
1072
  }
885
1073
  else if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featureTimeoutId))))
@@ -892,7 +1080,7 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
892
1080
  if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillIntlId))))
893
1081
  {
894
1082
  const char *defineIntl = "Object.defineProperty(globalThis, 'Intl', { value:{} });\n";
895
- JSValue j_defineIntl = JS_Eval(data->context, defineIntl, strlen(defineIntl), "<vm>", JS_EVAL_TYPE_GLOBAL);
1083
+ JSValue j_defineIntl = JS_Eval(data->context, defineIntl, strlen(defineIntl), vmInternalFilename, JS_EVAL_TYPE_GLOBAL);
896
1084
  JS_FreeValue(data->context, j_defineIntl);
897
1085
 
898
1086
  JSValue j_polyfillIntlResult = load_polyfill_bytecode(data->context, &qjsc_polyfill_intl_en_min, qjsc_polyfill_intl_en_min_size);
@@ -984,6 +1172,15 @@ static void check_oom_poisoned(VMData *data)
984
1172
  }
985
1173
  }
986
1174
 
1175
+ static void check_disposed(VMData *data)
1176
+ {
1177
+ if (data->disposed)
1178
+ {
1179
+ VALUE r_msg = rb_str_new2("VM has been disposed; create a new Quickjs::VM");
1180
+ rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, r_msg, Qnil));
1181
+ }
1182
+ }
1183
+
987
1184
  // Validate that r_code is a String and resolve the :filename option (default "<code>")
988
1185
  // from the keyword-args hash that rb_scan_args(... "1:") collects. Both eval_code and
989
1186
  // compile share this argument shape.
@@ -1014,6 +1211,7 @@ static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
1014
1211
  VMData *data;
1015
1212
  TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1016
1213
 
1214
+ check_disposed(data);
1017
1215
  check_oom_poisoned(data);
1018
1216
 
1019
1217
  VALUE r_code, r_opts;
@@ -1052,6 +1250,8 @@ static VALUE vm_m_compile(int argc, VALUE *argv, VALUE r_self)
1052
1250
  VMData *data;
1053
1251
  TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1054
1252
 
1253
+ check_disposed(data);
1254
+
1055
1255
  VALUE r_code, r_opts;
1056
1256
  rb_scan_args(argc, argv, "1:", &r_code, &r_opts);
1057
1257
  const char *filename = parse_code_and_filename(r_code, r_opts);
@@ -1086,6 +1286,8 @@ static VALUE vm_m_evalBytecode(VALUE r_self, VALUE r_bytecode)
1086
1286
  VMData *data;
1087
1287
  TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1088
1288
 
1289
+ check_disposed(data);
1290
+
1089
1291
  if (!RB_TYPE_P(r_bytecode, T_STRING))
1090
1292
  {
1091
1293
  VALUE r_class = rb_class_name(CLASS_OF(r_bytecode));
@@ -1127,6 +1329,8 @@ static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
1127
1329
  VMData *data;
1128
1330
  TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1129
1331
 
1332
+ check_disposed(data);
1333
+
1130
1334
  if (RB_TYPE_P(r_name, T_ARRAY))
1131
1335
  {
1132
1336
  long path_len = RARRAY_LEN(r_name);
@@ -1170,7 +1374,7 @@ static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
1170
1374
  {
1171
1375
  VALUE r_first_str = rb_funcall(RARRAY_AREF(r_name, 0), rb_intern("to_s"), 0);
1172
1376
  const char *first_seg = StringValueCStr(r_first_str);
1173
- j_parent = JS_Eval(data->context, first_seg, strlen(first_seg), "<vm>", JS_EVAL_TYPE_GLOBAL);
1377
+ j_parent = JS_Eval(data->context, first_seg, strlen(first_seg), vmInternalFilename, JS_EVAL_TYPE_GLOBAL);
1174
1378
 
1175
1379
  if (JS_IsException(j_parent) || !JS_IsObject(j_parent))
1176
1380
  {
@@ -1247,6 +1451,7 @@ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
1247
1451
  VMData *data;
1248
1452
  TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1249
1453
 
1454
+ check_disposed(data);
1250
1455
  check_oom_poisoned(data);
1251
1456
 
1252
1457
  JSValue j_this = JS_UNDEFINED;
@@ -1307,7 +1512,7 @@ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
1307
1512
  const char *first_seg = StringValueCStr(r_first_str);
1308
1513
 
1309
1514
  // JS_Eval accesses both global object properties and lexical (const/let) bindings
1310
- JSValue j_cur = JS_Eval(data->context, first_seg, strlen(first_seg), "<vm>", JS_EVAL_TYPE_GLOBAL);
1515
+ JSValue j_cur = JS_Eval(data->context, first_seg, strlen(first_seg), vmInternalFilename, JS_EVAL_TYPE_GLOBAL);
1311
1516
  if (JS_IsException(j_cur))
1312
1517
  return to_rb_value(data->context, j_cur); // raises
1313
1518
 
@@ -1384,6 +1589,11 @@ static VALUE vm_m_set_module_loader(VALUE r_self, VALUE r_loader)
1384
1589
  rb_raise(rb_eTypeError, "module_loader must be a Proc or nil");
1385
1590
 
1386
1591
  data->module_loader = r_loader;
1592
+ // Stale entries from the previous loader's policy would survive the
1593
+ // swap and silently shadow the new behavior.
1594
+ rb_hash_clear(data->module_resolution_cache);
1595
+ rb_hash_clear(data->module_source_cache);
1596
+ register_module_loader_funcs(data);
1387
1597
  return r_loader;
1388
1598
  }
1389
1599
 
@@ -1394,6 +1604,17 @@ static VALUE vm_m_get_module_loader(VALUE r_self)
1394
1604
  return data->module_loader;
1395
1605
  }
1396
1606
 
1607
+ static VALUE vm_m_on_unhandled_rejection(VALUE r_self)
1608
+ {
1609
+ rb_need_block();
1610
+
1611
+ VMData *data;
1612
+ TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1613
+
1614
+ data->on_unhandled_rejection = rb_block_proc();
1615
+ return Qnil;
1616
+ }
1617
+
1397
1618
  static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
1398
1619
  {
1399
1620
  VALUE r_import_string, r_opts;
@@ -1408,12 +1629,17 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
1408
1629
  rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, r_error_message, Qnil));
1409
1630
  return Qnil;
1410
1631
  }
1632
+ if (!NIL_P(r_from) && !NIL_P(r_filename))
1633
+ rb_raise(rb_eArgError, "pass either from: (inline source) or filename: (loader-resolved), not both");
1411
1634
  VALUE r_custom_exposure = rb_hash_aref(r_opts, ID2SYM(rb_intern("code_to_expose")));
1412
1635
 
1413
1636
  VMData *data;
1414
1637
  TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1415
1638
 
1639
+ check_disposed(data);
1640
+
1416
1641
  char *filename;
1642
+ VALUE r_seeded_key = Qnil;
1417
1643
  if (!NIL_P(r_filename))
1418
1644
  {
1419
1645
  filename = StringValueCStr(r_filename);
@@ -1429,6 +1655,18 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
1429
1655
  return to_rb_value(data->context, module);
1430
1656
  }
1431
1657
  js_module_set_import_meta(data->context, module, TRUE, FALSE);
1658
+ // The bridge module below will `import` this filename; without a
1659
+ // resolution-cache seed, our normalize hook would ask the user's
1660
+ // module_loader for it (and fail), even though QuickJS already has
1661
+ // the module loaded from the JS_Eval just above. Each `from:` call
1662
+ // mints a fresh random filename, so we also delete the entry once
1663
+ // the bridge eval finishes — otherwise the cache grows unboundedly.
1664
+ if (!NIL_P(data->module_loader))
1665
+ {
1666
+ VALUE r_filename_str = rb_str_new_cstr(filename);
1667
+ r_seeded_key = rb_ary_new3(2, r_filename_str, rb_str_new2(vmInternalFilename));
1668
+ rb_hash_aset(data->module_resolution_cache, r_seeded_key, r_filename_str);
1669
+ }
1432
1670
  JS_FreeValue(data->context, module);
1433
1671
  }
1434
1672
 
@@ -1456,9 +1694,21 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
1456
1694
  char *result = (char *)malloc(length + 1);
1457
1695
  snprintf(result, length + 1, importAndGlobalizeModule, import_name, filename, globalize);
1458
1696
 
1459
- JSValue j_codeResult = JS_Eval(data->context, result, strlen(result), "<vm>", JS_EVAL_TYPE_MODULE);
1697
+ JSValue j_codeResult = JS_Eval(data->context, result, strlen(result), vmInternalFilename, JS_EVAL_TYPE_MODULE);
1460
1698
  free(result);
1461
- JS_FreeValue(data->context, j_codeResult);
1699
+ if (JS_IsException(j_codeResult))
1700
+ return to_rb_value(data->context, j_codeResult);
1701
+
1702
+ // Module eval returns a Promise. Awaiting it surfaces top-level throws,
1703
+ // rejected dynamic imports, and rejected top-level awaits as Ruby
1704
+ // exceptions instead of silently dropping them.
1705
+ JSValue j_awaited = js_std_await(data->context, j_codeResult);
1706
+ if (JS_IsException(j_awaited))
1707
+ return to_rb_value(data->context, j_awaited);
1708
+ JS_FreeValue(data->context, j_awaited);
1709
+
1710
+ if (!NIL_P(r_seeded_key))
1711
+ rb_hash_delete(data->module_resolution_cache, r_seeded_key);
1462
1712
 
1463
1713
  return Qtrue;
1464
1714
  }
@@ -1483,10 +1733,14 @@ RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
1483
1733
  rb_define_method(r_class_vm, "import", vm_m_import, -1);
1484
1734
  rb_define_method(r_class_vm, "module_loader", vm_m_get_module_loader, 0);
1485
1735
  rb_define_method(r_class_vm, "module_loader=", vm_m_set_module_loader, 1);
1736
+ rb_define_method(r_class_vm, "on_unhandled_rejection", vm_m_on_unhandled_rejection, 0);
1486
1737
  rb_define_method(r_class_vm, "on_log", vm_m_on_log, 0);
1487
1738
  rb_define_method(r_class_vm, "memory_usage", vm_m_memoryUsage, 0);
1488
1739
  rb_define_method(r_class_vm, "gc!", vm_m_runGC, 0);
1489
1740
  rb_define_method(r_class_vm, "memory_poisoned?", vm_m_memoryPoisoned, 0);
1741
+ rb_define_method(r_class_vm, "dispose!", vm_m_dispose, 0);
1742
+ rb_define_method(r_class_vm, "disposed?", vm_m_disposed, 0);
1743
+ rb_define_method(r_class_vm, "drain_jobs!", vm_m_drainJobs, 0);
1490
1744
  r_define_log_class(r_class_vm);
1491
1745
  }
1492
1746
 
@@ -1494,6 +1748,7 @@ static VALUE vm_m_memoryUsage(VALUE r_self)
1494
1748
  {
1495
1749
  VMData *data;
1496
1750
  TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1751
+ check_disposed(data);
1497
1752
  JSMemoryUsage s;
1498
1753
  JS_ComputeMemoryUsage(JS_GetRuntime(data->context), &s);
1499
1754
  VALUE h = rb_hash_new();
@@ -1516,13 +1771,90 @@ static VALUE vm_m_runGC(VALUE r_self)
1516
1771
  {
1517
1772
  VMData *data;
1518
1773
  TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1774
+ check_disposed(data);
1519
1775
  JS_RunGC(JS_GetRuntime(data->context));
1520
1776
  return Qnil;
1521
1777
  }
1522
1778
 
1779
+ static VALUE vm_m_drainJobs(VALUE r_self)
1780
+ {
1781
+ VMData *data;
1782
+ TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1783
+
1784
+ check_oom_poisoned(data);
1785
+
1786
+ JSRuntime *runtime = JS_GetRuntime(data->context);
1787
+ if (!JS_IsJobPending(runtime))
1788
+ return INT2NUM(0);
1789
+
1790
+ arm_eval_timer(data);
1791
+
1792
+ int executed = 0;
1793
+ for (;;)
1794
+ {
1795
+ int err = JS_ExecutePendingJob(runtime, NULL);
1796
+ if (err == 0)
1797
+ break;
1798
+ if (err < 0)
1799
+ return to_rb_value(data->context, JS_EXCEPTION); // raises
1800
+ executed++;
1801
+ }
1802
+ return INT2NUM(executed);
1803
+ }
1804
+
1523
1805
  static VALUE vm_m_memoryPoisoned(VALUE r_self)
1524
1806
  {
1525
1807
  VMData *data;
1526
1808
  TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1527
1809
  return data->oom_poisoned ? Qtrue : Qfalse;
1528
1810
  }
1811
+
1812
+ // JS_FreeContext + JS_FreeRuntime walk the entire heap to run finalisers.
1813
+ // On a VM with polyfills loaded this can be tens of milliseconds — run it
1814
+ // without the GVL so other Ruby threads (e.g. the next pool builder) keep
1815
+ // progressing. Safe to release because nothing in the teardown path calls
1816
+ // back into Ruby: module_loader, console, and define_function callbacks
1817
+ // only fire during JS execution, not during free.
1818
+ static void *vm_dispose_no_gvl(void *p)
1819
+ {
1820
+ vm_teardown_context((JSContext *)p);
1821
+ return NULL;
1822
+ }
1823
+
1824
+ static VALUE vm_m_dispose(VALUE r_self)
1825
+ {
1826
+ VMData *data;
1827
+ TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1828
+
1829
+ if (data->disposed)
1830
+ return Qnil;
1831
+
1832
+ if (!JS_IsUndefined(data->j_file_proxy_creator))
1833
+ {
1834
+ JS_FreeValue(data->context, data->j_file_proxy_creator);
1835
+ data->j_file_proxy_creator = JS_UNDEFINED;
1836
+ }
1837
+
1838
+ // Mark disposed before releasing the GVL so a concurrent dfree finds
1839
+ // disposed=true and skips its own teardown.
1840
+ data->disposed = true;
1841
+
1842
+ rb_thread_call_without_gvl(vm_dispose_no_gvl, data->context, NULL, NULL);
1843
+
1844
+ // Drop references to user-supplied closures so Ruby GC can reclaim them
1845
+ // (and anything they captured) before the wrapping VM object itself is
1846
+ // collected. Matters for pool-rebuild workloads that dispose eagerly.
1847
+ data->defined_functions = rb_hash_new();
1848
+ data->alive_objects = rb_hash_new();
1849
+ data->log_listener = Qnil;
1850
+ data->module_loader = Qnil;
1851
+
1852
+ return Qnil;
1853
+ }
1854
+
1855
+ static VALUE vm_m_disposed(VALUE r_self)
1856
+ {
1857
+ VMData *data;
1858
+ TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1859
+ return data->disposed ? Qtrue : Qfalse;
1860
+ }