@mikrojs/native 0.21.1-next.20260927212749 → 0.22.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.
@@ -20,6 +20,7 @@ struct InspectOpts {
20
20
  std::vector<void*> seen;
21
21
  /* Nesting of [cause] rendering, capped like mik__report_uncaught */
22
22
  int cause_level = 0;
23
+ const MIKInspectCache* cache = nullptr;
23
24
  };
24
25
 
25
26
  /* ── Helpers ─────────────────────────────────────────────────────── */
@@ -106,23 +107,10 @@ static JSValue mik__custom_inspect_cb(JSContext* ctx, JSValue /*this_val*/, int
106
107
  * Returns true and sets `out` if custom inspect was used. */
107
108
  static bool try_custom_inspect(JSContext* ctx, JSValue value, InspectOpts& opts, int depth,
108
109
  std::string& out) {
109
- JSValue global = JS_GetGlobalObject(ctx);
110
- JSValue symbol_ctor = JS_GetPropertyStr(ctx, global, "Symbol");
111
- JSValue for_fn = JS_GetPropertyStr(ctx, symbol_ctor, "for");
112
-
113
- JSValue key_str = JS_NewString(ctx, "mikrojs.inspect");
114
- JSValue inspect_sym = JS_Call(ctx, for_fn, symbol_ctor, 1, &key_str);
115
- JS_FreeValue(ctx, key_str);
116
- JS_FreeValue(ctx, for_fn);
117
-
118
110
  bool used = false;
119
111
 
120
- if (JS_IsException(inspect_sym)) {
121
- drain_exception(ctx);
122
- } else {
123
- JSAtom sym_atom = JS_ValueToAtom(ctx, inspect_sym);
124
- JSValue custom_fn = JS_GetProperty(ctx, value, sym_atom);
125
- JS_FreeAtom(ctx, sym_atom);
112
+ if (opts.cache->inspect_symbol != JS_ATOM_NULL) {
113
+ JSValue custom_fn = JS_GetProperty(ctx, value, opts.cache->inspect_symbol);
126
114
  if (JS_IsException(custom_fn)) drain_exception(ctx);
127
115
 
128
116
  if (JS_IsFunction(ctx, custom_fn)) {
@@ -153,9 +141,6 @@ static bool try_custom_inspect(JSContext* ctx, JSValue value, InspectOpts& opts,
153
141
  }
154
142
  JS_FreeValue(ctx, custom_fn);
155
143
  }
156
- JS_FreeValue(ctx, inspect_sym);
157
- JS_FreeValue(ctx, symbol_ctor);
158
- JS_FreeValue(ctx, global);
159
144
 
160
145
  return used;
161
146
  }
@@ -206,16 +191,11 @@ static std::string escape_string(const char* str, size_t len) {
206
191
  return out;
207
192
  }
208
193
 
209
- /* ── Type detection via Object.prototype.toString ────────────────── */
194
+ /* ── Type detection ──────────────────────────────────────────────── */
210
195
 
211
- static std::string get_object_type(JSContext* ctx, JSValue value) {
212
- /* Call Object.prototype.toString.call(value) */
213
- JSValue global = JS_GetGlobalObject(ctx);
214
- JSValue object_ctor = JS_GetPropertyStr(ctx, global, "Object");
215
- JSValue prototype = JS_GetPropertyStr(ctx, object_ctor, "prototype");
216
- JSValue to_string = JS_GetPropertyStr(ctx, prototype, "toString");
217
-
218
- JSValue result = JS_Call(ctx, to_string, value, 0, nullptr);
196
+ /* Tag from the engine's Object.prototype.toString, "Foo" out of "[object Foo]" */
197
+ static std::string to_string_type(JSContext* ctx, JSValue value) {
198
+ JSValue result = JS_ToObjectString(ctx, value);
219
199
 
220
200
  std::string type;
221
201
  if (JS_IsException(result)) {
@@ -236,11 +216,47 @@ static std::string get_object_type(JSContext* ctx, JSValue value) {
236
216
  }
237
217
 
238
218
  JS_FreeValue(ctx, result);
239
- JS_FreeValue(ctx, to_string);
240
- JS_FreeValue(ctx, prototype);
241
- JS_FreeValue(ctx, object_ctor);
242
- JS_FreeValue(ctx, global);
219
+ return type;
220
+ }
243
221
 
222
+ /* Class checks plus one Symbol.toStringTag read give toString's tag; other
223
+ * classes go through toString. */
224
+ static std::string get_object_type(JSContext* ctx, JSValue value, const MIKInspectCache& cache) {
225
+ const char* builtin = nullptr;
226
+ if (JS_IsError(value)) {
227
+ builtin = "Error";
228
+ } else if (JS_IsDate(value)) {
229
+ builtin = "Date";
230
+ } else if (JS_IsRegExp(value)) {
231
+ builtin = "RegExp";
232
+ } else if (JS_GetClassID(value) == cache.object_class_id || JS_IsMap(value) ||
233
+ JS_IsSet(value) || JS_IsWeakMap(value) || JS_IsWeakSet(value) ||
234
+ JS_IsWeakRef(value) || JS_IsPromise(value) || JS_IsArrayBuffer(value) ||
235
+ JS_IsDataView(value) || JS_GetTypedArrayType(value) >= 0) {
236
+ builtin = "Object";
237
+ }
238
+ if (!builtin || cache.to_string_tag == JS_ATOM_NULL) {
239
+ return to_string_type(ctx, value);
240
+ }
241
+
242
+ JSValue tag = JS_GetProperty(ctx, value, cache.to_string_tag);
243
+ std::string type;
244
+ if (JS_IsException(tag)) {
245
+ /* A throwing Symbol.toStringTag getter or proxy trap */
246
+ drain_exception(ctx);
247
+ } else if (JS_IsString(tag)) {
248
+ const char* str = JS_ToCString(ctx, tag);
249
+ if (str) {
250
+ /* An empty tag gives "[object ]", which to_string_type keeps whole */
251
+ type = str[0] ? str : "[object ]";
252
+ JS_FreeCString(ctx, str);
253
+ } else {
254
+ drain_exception(ctx);
255
+ }
256
+ } else {
257
+ type = builtin;
258
+ }
259
+ JS_FreeValue(ctx, tag);
244
260
  return type;
245
261
  }
246
262
 
@@ -275,28 +291,31 @@ static std::string inspect_array_items(JSContext* ctx, JSValue arr, uint32_t len
275
291
 
276
292
  /* ── Inspect object properties ───────────────────────────────────── */
277
293
 
278
- static std::string inspect_properties(JSContext* ctx, JSValue obj, InspectOpts& opts, int depth,
279
- const char* const* skip_keys = nullptr,
280
- size_t skip_count = 0) {
281
- JSPropertyEnum* ptab = nullptr;
282
- uint32_t plen = 0;
283
-
294
+ /* Own string keys to render (enumerable only unless showHidden). On failure
295
+ * drains the exception and returns false. Free with JS_FreePropertyEnum. */
296
+ static bool get_own_keys(JSContext* ctx, JSValue obj, const InspectOpts& opts,
297
+ JSPropertyEnum** ptab, uint32_t* plen) {
284
298
  int flags = JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY;
285
299
  if (opts.show_hidden) {
286
300
  flags = JS_GPN_STRING_MASK;
287
301
  }
288
302
 
289
- if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, obj, flags) != 0) {
303
+ if (JS_GetOwnPropertyNames(ctx, ptab, plen, obj, flags) != 0) {
290
304
  drain_exception(ctx);
291
- return "";
305
+ return false;
292
306
  }
307
+ return true;
308
+ }
293
309
 
310
+ static std::string inspect_properties(JSContext* ctx, JSValue obj, const JSPropertyEnum* ptab,
311
+ uint32_t plen, InspectOpts& opts, int depth,
312
+ const char* const* skip_keys = nullptr,
313
+ size_t skip_count = 0) {
294
314
  std::string out;
295
315
  bool first = true;
296
316
  for (uint32_t i = 0; i < plen; i++) {
297
317
  const char* key = JS_AtomToCString(ctx, ptab[i].atom);
298
318
  if (!key) {
299
- JS_FreeAtom(ctx, ptab[i].atom);
300
319
  continue;
301
320
  }
302
321
 
@@ -315,7 +334,7 @@ static std::string inspect_properties(JSContext* ctx, JSValue obj, InspectOpts&
315
334
  if (!first) out += ", ";
316
335
  first = false;
317
336
 
318
- JSValue val = JS_GetPropertyStr(ctx, obj, key);
337
+ JSValue val = JS_GetProperty(ctx, obj, ptab[i].atom);
319
338
  out += quote_key(key);
320
339
  out += ": ";
321
340
  if (JS_IsException(val)) {
@@ -327,9 +346,7 @@ static std::string inspect_properties(JSContext* ctx, JSValue obj, InspectOpts&
327
346
  }
328
347
 
329
348
  JS_FreeCString(ctx, key);
330
- JS_FreeAtom(ctx, ptab[i].atom);
331
349
  }
332
- js_free(ctx, ptab);
333
350
  return out;
334
351
  }
335
352
 
@@ -375,12 +392,8 @@ static std::string inspect_function(JSContext* ctx, JSValue value, InspectOpts&
375
392
 
376
393
  /* Check for Symbol.toStringTag */
377
394
  std::string func_type = "Function";
378
- JSValue global = JS_GetGlobalObject(ctx);
379
- JSValue symbol_ctor = JS_GetPropertyStr(ctx, global, "Symbol");
380
- JSValue tag_sym = JS_GetPropertyStr(ctx, symbol_ctor, "toStringTag");
381
- if (!JS_IsUndefined(tag_sym)) {
382
- JSAtom tag_atom = JS_ValueToAtom(ctx, tag_sym);
383
- JSValue tag_val = JS_GetProperty(ctx, value, tag_atom);
395
+ if (opts.cache->to_string_tag != JS_ATOM_NULL) {
396
+ JSValue tag_val = JS_GetProperty(ctx, value, opts.cache->to_string_tag);
384
397
  if (JS_IsException(tag_val)) drain_exception(ctx);
385
398
  if (JS_IsString(tag_val)) {
386
399
  const char* tag = JS_ToCString(ctx, tag_val);
@@ -390,11 +403,7 @@ static std::string inspect_function(JSContext* ctx, JSValue value, InspectOpts&
390
403
  }
391
404
  }
392
405
  JS_FreeValue(ctx, tag_val);
393
- JS_FreeAtom(ctx, tag_atom);
394
406
  }
395
- JS_FreeValue(ctx, tag_sym);
396
- JS_FreeValue(ctx, symbol_ctor);
397
- JS_FreeValue(ctx, global);
398
407
 
399
408
  if (name && name[0]) {
400
409
  result = "[" + func_type + " " + name + "]";
@@ -554,8 +563,14 @@ static std::string inspect_error(JSContext* ctx, JSValue value, InspectOpts& opt
554
563
  static const char* const error_keys[] = {"stack", "line", "column", "name",
555
564
  "message", "fileName", "lineNumber", "columnNumber",
556
565
  "number", "description", "cause"};
557
- std::string props =
558
- inspect_properties(ctx, value, opts, depth, error_keys, countof(error_keys));
566
+ std::string props;
567
+ JSPropertyEnum* ptab = nullptr;
568
+ uint32_t plen = 0;
569
+ if (get_own_keys(ctx, value, opts, &ptab, &plen)) {
570
+ props = inspect_properties(ctx, value, ptab, plen, opts, depth, error_keys,
571
+ countof(error_keys));
572
+ JS_FreePropertyEnum(ctx, ptab, plen);
573
+ }
559
574
  if (!props.empty()) {
560
575
  result += " { " + props + " }";
561
576
  }
@@ -708,15 +723,10 @@ static std::string inspect_set(JSContext* ctx, JSValue value, InspectOpts& opts,
708
723
  if (is_seen(opts, value)) return "[Circular]";
709
724
  opts.seen.push_back(JS_VALUE_GET_PTR(value));
710
725
 
711
- /* Get values via forEach */
712
- JSValue for_each_fn = JS_GetPropertyStr(ctx, value, "forEach");
713
-
714
- /* Build array of values to inspect */
715
726
  std::string items;
716
727
  JSValue values_fn = JS_GetPropertyStr(ctx, value, "values");
717
728
  JSValue iterator = JS_Call(ctx, values_fn, value, 0, nullptr);
718
729
  JS_FreeValue(ctx, values_fn);
719
- JS_FreeValue(ctx, for_each_fn);
720
730
 
721
731
  if (JS_IsException(iterator)) {
722
732
  drain_exception(ctx);
@@ -799,30 +809,20 @@ static std::string inspect_typed_array(JSContext* ctx, JSValue value, const std:
799
809
  static std::string inspect_object(JSContext* ctx, JSValue value, InspectOpts& opts, int depth) {
800
810
  if (is_seen(opts, value)) return "[Circular]";
801
811
 
802
- /* Check for empty object */
803
812
  JSPropertyEnum* ptab = nullptr;
804
813
  uint32_t plen = 0;
805
- int flags = JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY;
806
- if (opts.show_hidden) {
807
- flags = JS_GPN_STRING_MASK;
808
- }
809
-
810
- if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, value, flags) != 0) {
811
- drain_exception(ctx);
814
+ if (!get_own_keys(ctx, value, opts, &ptab, &plen)) return "{}";
815
+ if (plen == 0) {
816
+ JS_FreePropertyEnum(ctx, ptab, plen);
812
817
  return "{}";
813
818
  }
814
- for (uint32_t i = 0; i < plen; i++) {
815
- JS_FreeAtom(ctx, ptab[i].atom);
816
- }
817
- js_free(ctx, ptab);
818
-
819
- if (plen == 0) return "{}";
820
819
 
821
820
  /* A cause field chains below the braces like an Error's, so a Result
822
821
  * error wrapped around another keeps its whole chain readable */
823
822
  static const char* const cause_key[] = {"cause"};
824
823
  opts.seen.push_back(JS_VALUE_GET_PTR(value));
825
- std::string props = inspect_properties(ctx, value, opts, depth, cause_key, 1);
824
+ std::string props = inspect_properties(ctx, value, ptab, plen, opts, depth, cause_key, 1);
825
+ JS_FreePropertyEnum(ctx, ptab, plen);
826
826
  std::string result = props.empty() ? "{}" : "{ " + props + " }";
827
827
  result += inspect_cause(ctx, value, opts, depth);
828
828
  opts.seen.pop_back();
@@ -915,9 +915,12 @@ static std::string inspect_value(JSContext* ctx, JSValue value, InspectOpts& opt
915
915
  }
916
916
 
917
917
  /* Objects — detect type */
918
- std::string type = get_object_type(ctx, value);
918
+ if (JS_IsArray(value)) {
919
+ return inspect_array(ctx, value, opts, depth - 1);
920
+ }
921
+ std::string type = get_object_type(ctx, value, *opts.cache);
919
922
 
920
- if (JS_IsArray(value) || type == "Array") {
923
+ if (type == "Array") {
921
924
  return inspect_array(ctx, value, opts, depth - 1);
922
925
  }
923
926
  if (JS_IsError(value) || type == "Error") {
@@ -955,16 +958,8 @@ static std::string inspect_value(JSContext* ctx, JSValue value, InspectOpts& opt
955
958
  /* Plain object vs class instance */
956
959
  JSValue proto = JS_GetPrototype(ctx, value);
957
960
  if (JS_IsException(proto)) drain_exception(ctx);
958
- JSValue global = JS_GetGlobalObject(ctx);
959
- JSValue obj_ctor = JS_GetPropertyStr(ctx, global, "Object");
960
- JSValue obj_proto = JS_GetPropertyStr(ctx, obj_ctor, "prototype");
961
-
962
961
  bool is_plain = JS_IsNull(proto) ||
963
- (JS_VALUE_GET_PTR(proto) == JS_VALUE_GET_PTR(obj_proto));
964
-
965
- JS_FreeValue(ctx, obj_proto);
966
- JS_FreeValue(ctx, obj_ctor);
967
- JS_FreeValue(ctx, global);
962
+ (JS_VALUE_GET_PTR(proto) == JS_VALUE_GET_PTR(opts.cache->object_proto));
968
963
  JS_FreeValue(ctx, proto);
969
964
 
970
965
  if (is_plain) {
@@ -982,6 +977,7 @@ std::string mik_inspect(JSContext* ctx, JSValue value, int depth, bool colors, b
982
977
  opts.truncate = 0; /* 0 = unlimited */
983
978
  opts.colors = colors;
984
979
  opts.show_hidden = show_hidden;
980
+ opts.cache = &MIK_GetRuntime(ctx)->inspect_cache;
985
981
  return inspect_value(ctx, value, opts, depth);
986
982
  }
987
983
 
@@ -1026,6 +1022,25 @@ int mik__inspect_module_init(JSContext* ctx, JSModuleDef* m) {
1026
1022
  }
1027
1023
 
1028
1024
  void mik__inspect_register(JSContext* ctx) {
1025
+ MIKInspectCache& cache = MIK_GetRuntime(ctx)->inspect_cache;
1026
+ /* is_global makes this the registered symbol Symbol.for returns */
1027
+ JSValue sym = JS_NewSymbol(ctx, "mikrojs.inspect", true);
1028
+ if (!JS_IsException(sym)) cache.inspect_symbol = JS_ValueToAtom(ctx, sym);
1029
+ JS_FreeValue(ctx, sym);
1030
+
1031
+ JSValue global = JS_GetGlobalObject(ctx);
1032
+ JSValue symbol_ctor = JS_GetPropertyStr(ctx, global, "Symbol");
1033
+ JSValue tag_sym = JS_GetPropertyStr(ctx, symbol_ctor, "toStringTag");
1034
+ if (JS_IsSymbol(tag_sym)) cache.to_string_tag = JS_ValueToAtom(ctx, tag_sym);
1035
+ JS_FreeValue(ctx, tag_sym);
1036
+ JS_FreeValue(ctx, symbol_ctor);
1037
+
1038
+ JSValue object_ctor = JS_GetPropertyStr(ctx, global, "Object");
1039
+ cache.object_proto = JS_GetPropertyStr(ctx, object_ctor, "prototype");
1040
+ cache.object_class_id = JS_GetClassID(cache.object_proto);
1041
+ JS_FreeValue(ctx, object_ctor);
1042
+ JS_FreeValue(ctx, global);
1043
+
1029
1044
  JSModuleDef* m = JS_NewCModule(ctx, "mikro/inspect", mik__inspect_module_init);
1030
1045
  if (m) {
1031
1046
  JS_AddModuleExport(ctx, m, "inspect");
@@ -71,6 +71,10 @@ extern "C" {
71
71
  #include "quickjs.h"
72
72
  }
73
73
 
74
+ /* Property names the per-value paths use, atomized once per runtime */
75
+ enum ObservableAtom { OBS_NEXT, OBS_DONE, OBS_VALUE, OBS_ATOM_COUNT };
76
+ static const char* const observable_atom_names[OBS_ATOM_COUNT] = {"next", "done", "value"};
77
+
74
78
  /* Per-runtime dispatch queue. Entries hold their own references (dup on
75
79
  * enqueue, freed after the drained delivery). The queue is only non-empty
76
80
  * while a dispatch is on the stack, so runtime teardown never sees values. */
@@ -83,6 +87,7 @@ struct MIKObservableDispatch {
83
87
  bool active = false;
84
88
  size_t head = 0;
85
89
  std::vector<Entry> queue;
90
+ JSAtom atoms[OBS_ATOM_COUNT] = {};
86
91
  };
87
92
 
88
93
  namespace {
@@ -234,6 +239,11 @@ static MIKObservableDispatch* dispatch_state(JSContext* ctx) {
234
239
  return mik_rt ? mik_rt->observable_dispatch : nullptr;
235
240
  }
236
241
 
242
+ /* JS_NewAtom(name) without hashing the name: dups the cached atom. Free with JS_FreeAtom. */
243
+ static JSAtom observable_atom(JSContext* ctx, ObservableAtom which) {
244
+ return JS_DupAtom(ctx, dispatch_state(ctx)->atoms[which]);
245
+ }
246
+
237
247
  /* True once a panic is armed. The producer's own callback keeps running (JS
238
248
  * cannot be stopped mid-function), so its later next/complete calls have to
239
249
  * become no-ops rather than delivering against crashed state. */
@@ -656,12 +666,14 @@ static JSValue get_iterator(JSContext* ctx, JSValueConst src) {
656
666
  * Sets *done = true if iteration is complete (and returns JS_UNDEFINED).
657
667
  * Returns JS_EXCEPTION on protocol error. Caller frees the returned value. */
658
668
  static JSValue iterator_next(JSContext* ctx, JSValueConst iterator, bool* done) {
659
- JSAtom next_atom = JS_NewAtom(ctx, "next");
669
+ JSAtom next_atom = observable_atom(ctx, OBS_NEXT);
660
670
  JSValue result = JS_Invoke(ctx, iterator, next_atom, 0, nullptr);
661
671
  JS_FreeAtom(ctx, next_atom);
662
672
  if (JS_IsException(result)) return result;
663
673
 
664
- JSValue done_val = JS_GetPropertyStr(ctx, result, "done");
674
+ JSAtom done_atom = observable_atom(ctx, OBS_DONE);
675
+ JSValue done_val = JS_GetProperty(ctx, result, done_atom);
676
+ JS_FreeAtom(ctx, done_atom);
665
677
  int done_int = JS_ToBool(ctx, done_val);
666
678
  JS_FreeValue(ctx, done_val);
667
679
  if (done_int < 0) {
@@ -674,7 +686,9 @@ static JSValue iterator_next(JSContext* ctx, JSValueConst iterator, bool* done)
674
686
  JS_FreeValue(ctx, result);
675
687
  return JS_UNDEFINED;
676
688
  }
677
- JSValue value = JS_GetPropertyStr(ctx, result, "value");
689
+ JSAtom value_atom = observable_atom(ctx, OBS_VALUE);
690
+ JSValue value = JS_GetProperty(ctx, result, value_atom);
691
+ JS_FreeAtom(ctx, value_atom);
678
692
  JS_FreeValue(ctx, result);
679
693
  return value;
680
694
  }
@@ -721,7 +735,7 @@ static JSValue from_iterable_subscribe(JSContext* ctx, JSValueConst this_val, in
721
735
  JSValue iterator = get_iterator(ctx, c->iterable);
722
736
  if (JS_IsException(iterator)) return iterator;
723
737
 
724
- JSAtom next_atom = JS_NewAtom(ctx, "next");
738
+ JSAtom next_atom = observable_atom(ctx, OBS_NEXT);
725
739
 
726
740
  bool done = false;
727
741
  while (!done) {
@@ -792,7 +806,7 @@ static JSValue from_promise_on_resolve(JSContext* ctx, JSValueConst this_val, in
792
806
  if (!sd || sd->closed) return JS_UNDEFINED;
793
807
 
794
808
  JSValue value = argc > 0 ? argv[0] : JS_UNDEFINED;
795
- JSAtom next_atom = JS_NewAtom(ctx, "next");
809
+ JSAtom next_atom = observable_atom(ctx, OBS_NEXT);
796
810
  invoke_safely(ctx, subscriber, next_atom, 1, &value);
797
811
  JS_FreeAtom(ctx, next_atom);
798
812
 
@@ -1008,7 +1022,7 @@ static JSValue multicast_emit_next(JSContext* ctx, JSValueConst this_val, int ar
1008
1022
  for (auto& s : m->subscribers) snapshot.push_back(JS_DupValue(ctx, s));
1009
1023
 
1010
1024
  JSValue value = argc > 0 ? argv[0] : JS_UNDEFINED;
1011
- JSAtom next_atom = JS_NewAtom(ctx, "next");
1025
+ JSAtom next_atom = observable_atom(ctx, OBS_NEXT);
1012
1026
  MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
1013
1027
  for (auto& s : snapshot) {
1014
1028
  auto* sd = static_cast<SubscriberData*>(JS_GetOpaque(s, subscriber_class_id));
@@ -1982,6 +1996,9 @@ int mik__observable_multicast_new(JSContext* ctx, JSValue* observable, JSValue*
1982
1996
  }
1983
1997
 
1984
1998
  void mik__observable_dispatch_free(MIKRuntime* mik_rt) {
1999
+ if (mik_rt->observable_dispatch) {
2000
+ for (JSAtom a : mik_rt->observable_dispatch->atoms) JS_FreeAtom(mik_rt->ctx, a);
2001
+ }
1985
2002
  delete mik_rt->observable_dispatch;
1986
2003
  mik_rt->observable_dispatch = nullptr;
1987
2004
  }
@@ -1992,6 +2009,9 @@ JSModuleDef* mik__observable_init(JSContext* ctx) {
1992
2009
  MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
1993
2010
  if (mik_rt && !mik_rt->observable_dispatch) {
1994
2011
  mik_rt->observable_dispatch = new MIKObservableDispatch();
2012
+ for (int i = 0; i < OBS_ATOM_COUNT; i++) {
2013
+ mik_rt->observable_dispatch->atoms[i] = JS_NewAtom(ctx, observable_atom_names[i]);
2014
+ }
1995
2015
  }
1996
2016
 
1997
2017
  /* The IDs are process-wide (MIK_NewClassID); the classes register once per runtime. */
@@ -10,6 +10,7 @@
10
10
  #include <vector>
11
11
 
12
12
  #include "mikrojs/cbor_helpers.h"
13
+ #include "mikrojs/mikrojs.h"
13
14
  #include "mikrojs/ota_config.h"
14
15
  #include "mikrojs/ota_slots.h"
15
16
  #include "mikrojs/platform.h"
@@ -388,6 +389,8 @@ void MIKOtaClient::Check(const MIKOtaCheckOptions& options, MIKOtaCheckSink sink
388
389
  return;
389
390
  }
390
391
  queue_.push_back(std::move(round));
392
+ /* Poll runs from the loop consumer, which may have run already this pass. */
393
+ MIK_Wake();
391
394
  }
392
395
 
393
396
  /* Floor for the check-in cadence: each round's TLS session leaves heap and
@@ -2,6 +2,7 @@
2
2
 
3
3
  #include <map>
4
4
 
5
+ #include "mikrojs/mikrojs.h"
5
6
  #include "mikrojs/utils.h"
6
7
 
7
8
  namespace mikrojs {
@@ -178,6 +179,8 @@ JSValue MIKOtaJsHooks::OnFulfilled(JSContext* ctx, JSValueConst this_val, int ar
178
179
  (void)this_val;
179
180
  MIKOtaJsHooks* hooks = take_self(ctx, func_data);
180
181
  if (!hooks) return JS_UNDEFINED;
182
+ /* A job settled the hook; the OTA consumer that polls it has run this pass. */
183
+ MIK_Wake();
181
184
  if (magic == 0) {
182
185
  /* Rejected: a failed hook, whichever half was running. */
183
186
  hooks->state_ = MIKOtaHookState::kFailed;
package/src/mik_repl.cpp CHANGED
@@ -62,6 +62,30 @@ bool mik__repl_is_protocol_mode(void) {
62
62
  return repl_protocol_mode && repl_active;
63
63
  }
64
64
 
65
+ /* Longest sleep between passes: a backstop against a producer that forgot
66
+ * MIK_Wake, not a poll period. */
67
+ static constexpr int64_t LOOP_WAIT_CAP_US = 100 * 1000;
68
+
69
+ /* Sleep until the next deadline, a wake, or the cap. Work due now yields
70
+ * instead; a paused or absent runtime waits for the cap (console RX wakes it). */
71
+ static void repl_wait_for_work(void) {
72
+ const MIKPlatform* platform = MIK_GetPlatform();
73
+ int64_t wake_us = LOOP_WAIT_CAP_US;
74
+ if (repl_mik_rt && !repl_paused) {
75
+ wake_us = mik__next_wake_us(repl_mik_rt);
76
+ if (wake_us == 0) {
77
+ platform->yield();
78
+ return;
79
+ }
80
+ if (wake_us < 0 || wake_us > LOOP_WAIT_CAP_US) wake_us = LOOP_WAIT_CAP_US;
81
+ }
82
+ if (platform->wait) {
83
+ platform->wait(wake_us);
84
+ } else {
85
+ platform->yield();
86
+ }
87
+ }
88
+
65
89
  /* ── TLV frame helpers ───────────────────────────────────────────── */
66
90
 
67
91
  /* Plain bool, not atomic — relies on the assumption that mik__proto_send
@@ -124,7 +148,6 @@ void mik__repl_proto_send_output(uint8_t msg_type, const void* data, size_t len)
124
148
  * reports the attached runtime has halted without a panic grace window
125
149
  * (e.g. an unhandled rejection in a test that crashed mid-execution). */
126
150
  bool mik__proto_read_exact(MIKReplTransport* transport, void* buf, size_t n) {
127
- const MIKPlatform* platform = MIK_GetPlatform();
128
151
  uint8_t* p = static_cast<uint8_t*>(buf);
129
152
  size_t total = 0;
130
153
  while (total < n) {
@@ -143,19 +166,12 @@ bool mik__proto_read_exact(MIKReplTransport* transport, void* buf, size_t n) {
143
166
  return false;
144
167
  }
145
168
  }
146
- /* Microtasks are deferred user JS: settling them re-enters .then
147
- * handlers that can write to the transport (console.log → MSG_LOG)
148
- * or touch the filesystem mid-deploy. Gate on pause too — and
149
- * on a pending panic-restart, since the runtime is on its way
150
- * out and no further user JS should fire on it. */
151
- if (repl_ctx && !repl_paused &&
152
- (!repl_mik_rt || repl_mik_rt->restart_at_us == 0)) {
153
- mik__execute_jobs(repl_ctx);
154
- }
169
+ /* Microtasks drain inside MIK_Loop, so none run while paused or
170
+ * while a panic-restart is pending. */
155
171
  /* A pumped job (e.g. the test runtime's __testFileDone) may
156
- * have signalled exit. Bail before yielding another time slice. */
172
+ * have signalled exit. Bail before sleeping again. */
157
173
  if (s_exit_serve_loop) return false;
158
- platform->yield();
174
+ repl_wait_for_work();
159
175
  }
160
176
  }
161
177
  return true;
@@ -264,8 +280,6 @@ static JSValue repl_eval_and_pump(JSContext* ctx, const char* code, size_t len)
264
280
  return result;
265
281
  }
266
282
 
267
- const MIKPlatform* platform = MIK_GetPlatform();
268
-
269
283
  for (;;) {
270
284
  JSPromiseStateEnum state = JS_PromiseState(ctx, result);
271
285
  if (state == JS_PROMISE_FULFILLED) {
@@ -296,8 +310,8 @@ static JSValue repl_eval_and_pump(JSContext* ctx, const char* code, size_t len)
296
310
  }
297
311
 
298
312
  MIK_Loop(repl_mik_rt);
299
- mik__execute_jobs(ctx);
300
- platform->yield();
313
+ /* The pass may have settled it (a job or a due timer): report now. */
314
+ if (JS_PromiseState(ctx, result) == JS_PROMISE_PENDING) repl_wait_for_work();
301
315
  }
302
316
  }
303
317
 
@@ -903,17 +917,19 @@ static std::vector<uint8_t> proto_complete(JSContext* ctx, const char* partial,
903
917
  /* ── Protocol-mode REPL ──────────────────────────────────────────── */
904
918
 
905
919
  /* Fills ready_buf/ready_len with CBOR device info:
906
- * {"chip": tstr, "id": tstr, "v": tstr, "board": tstr, "fw": tstr (when built
907
- * with MIK_BOARD_NAME), "name": tstr (only when named), "features": [tstr] (when
908
- * built with MIK_FW_FEATURES; host builds omit it), "natives": [tstr]}.
920
+ * {"chip": tstr, "id": tstr, "v": tstr, "board": tstr, "proto": uint
921
+ * (MIK_PROTO_REV), "fw": tstr (when built with MIK_BOARD_NAME), "name": tstr
922
+ * (only when named), "features": [tstr] (when built with MIK_FW_FEATURES; host
923
+ * builds omit it), "natives": [tstr]}.
909
924
  * `board` is mik__board_name(). `features` lists the firmware features the
910
925
  * build compiled in (from the comma-separated MIK_FW_FEATURES define), so the
911
926
  * host knows which feature-gated builtins this firmware carries. `natives`
912
927
  * lists the registered C modules outside the core namespace (package native
913
928
  * modules compiled in, e.g. "@mikrojs/drivers/sh8601"; empty on a generic
914
929
  * build), so the host can refuse an app importing one this firmware lacks.
915
- * `fw` is the firmware identity: the board name the build set (MIK_BOARD_NAME),
916
- * which `board` also reports. The host only auto-flashes its bundled image over
930
+ * `fw` is the firmware identity: the name the build set (MIK_BOARD_NAME). `board`
931
+ * reports the same unless `mikro flash` wrote a board's name into a generic
932
+ * image (mik__board_name). The host only auto-flashes its bundled image over
917
933
  * a device whose identity matches that image's name; omitting it reads as
918
934
  * firmware predating identity reporting, which the host treats as its own
919
935
  * bundled firmware.
@@ -949,7 +965,7 @@ static size_t encode_ready(uint8_t* buf, size_t cap, const char* chip, const cha
949
965
  static uint8_t measure_base;
950
966
  nanocbor_encoder_t enc;
951
967
  nanocbor_encoder_init(&enc, buf ? buf : &measure_base, buf ? cap : 0);
952
- nanocbor_fmt_map(&enc, 4 + (fw ? 1 : 0) + (name ? 1 : 0) + (features ? 1 : 0) +
968
+ nanocbor_fmt_map(&enc, 5 + (fw ? 1 : 0) + (name ? 1 : 0) + (features ? 1 : 0) +
953
969
  (natives ? 1 : 0) + (mem ? 3 : 0));
954
970
  nanocbor_put_tstr(&enc, "chip");
955
971
  nanocbor_put_tstr(&enc, chip);
@@ -959,6 +975,8 @@ static size_t encode_ready(uint8_t* buf, size_t cap, const char* chip, const cha
959
975
  nanocbor_put_tstr(&enc, version);
960
976
  nanocbor_put_tstr(&enc, "board");
961
977
  nanocbor_put_tstr(&enc, board);
978
+ nanocbor_put_tstr(&enc, "proto");
979
+ nanocbor_fmt_uint(&enc, MIK_PROTO_REV);
962
980
  if (fw) {
963
981
  nanocbor_put_tstr(&enc, "fw");
964
982
  nanocbor_put_tstr(&enc, fw);
@@ -1324,9 +1342,6 @@ void MIK_ProtocolServeLoop(void) {
1324
1342
  if (repl_mik_rt && !repl_paused) {
1325
1343
  MIK_Loop(repl_mik_rt);
1326
1344
  }
1327
- if (ctx && !repl_paused) {
1328
- mik__execute_jobs(ctx);
1329
- }
1330
1345
  }
1331
1346
  }
1332
1347