@mikrojs/native 0.20.1 → 0.21.0-next.20260913195055

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.
Files changed (56) hide show
  1. package/CMakeLists.txt +3 -1
  2. package/cmake/mikrojs_bytecode.cmake +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/runtime/result/native-result.node-shim.js.map +1 -1
  6. package/dist/runtime/result/types.d.ts +5 -5
  7. package/dist/runtime/result/types.d.ts.map +1 -1
  8. package/dist/types.d.ts +1 -1
  9. package/dist/types.d.ts.map +1 -1
  10. package/include/mikrojs/mikrojs.h +26 -2
  11. package/include/mikrojs/ota_env.h +8 -6
  12. package/include/mikrojs/ota_policy.h +16 -0
  13. package/include/mikrojs/private.h +13 -0
  14. package/include/mikrojs/utils.h +24 -0
  15. package/package.json +10 -10
  16. package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
  17. package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
  18. package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
  19. package/runtime/gpio/types.ts +116 -0
  20. package/runtime/i2c/types.ts +25 -31
  21. package/runtime/i2s/types.ts +23 -12
  22. package/runtime/internal.d.ts +1 -174
  23. package/runtime/kv/shared.ts +13 -11
  24. package/runtime/neopixel/types.ts +24 -6
  25. package/runtime/observable/native-observable.node-shim.ts +73 -37
  26. package/runtime/observable/native-operators.node-shim.ts +417 -0
  27. package/runtime/observable/observable.ts +76 -1
  28. package/runtime/observable/operators.ts +156 -89
  29. package/runtime/observable/types.ts +61 -6
  30. package/runtime/pwm/types.ts +31 -11
  31. package/runtime/result/native-result.node-shim.ts +1 -1
  32. package/runtime/result/types.ts +5 -5
  33. package/runtime/sleep/types.ts +10 -8
  34. package/runtime/spi/types.ts +16 -14
  35. package/runtime/uart/types.ts +27 -15
  36. package/src/gpio_claim.cpp +63 -0
  37. package/src/mik_abort.cpp +1 -2
  38. package/src/mik_console.cpp +192 -115
  39. package/src/mik_http_client.cpp +3 -14
  40. package/src/mik_inspect.cpp +86 -9
  41. package/src/mik_observable.cpp +1028 -80
  42. package/src/mik_ota_client.cpp +5 -2
  43. package/src/mik_ota_policy.cpp +65 -18
  44. package/src/mik_result.cpp +10 -9
  45. package/src/mikrojs.cpp +26 -13
  46. package/src/modules.cpp +17 -6
  47. package/src/timers.cpp +6 -2
  48. package/src/utils.cpp +106 -0
  49. package/runtime/i2c/i2c.ts +0 -35
  50. package/runtime/i2s/i2s.ts +0 -46
  51. package/runtime/neopixel/neopixel.ts +0 -38
  52. package/runtime/pin/pin.ts +0 -51
  53. package/runtime/pin/types.ts +0 -49
  54. package/runtime/pwm/pwm.ts +0 -32
  55. package/runtime/spi/spi.ts +0 -31
  56. package/runtime/uart/uart.ts +0 -52
@@ -0,0 +1,63 @@
1
+ #include <cstring>
2
+ #include <string>
3
+
4
+ #include "mikrojs/mikrojs.h"
5
+ #include "mikrojs/private.h"
6
+ #include "mikrojs/utils.h"
7
+
8
+ /* Runtime-wide exclusive GPIO pin claims. Only touched from the JS task, so no
9
+ * locking. Owners are static strings, so the table never frees anything. */
10
+
11
+ #define MIK__GPIO_MAX 64
12
+
13
+ static const char* s_owner[MIK__GPIO_MAX] = {};
14
+
15
+ static bool mik__gpio_tracked(int gpio) {
16
+ return gpio >= 0 && gpio < MIK__GPIO_MAX;
17
+ }
18
+
19
+ bool MIK_ClaimGpio(int gpio, const char* owner) {
20
+ if (!owner) return false;
21
+ if (!mik__gpio_tracked(gpio)) return true;
22
+ if (s_owner[gpio]) return false;
23
+ s_owner[gpio] = owner;
24
+ return true;
25
+ }
26
+
27
+ void MIK_ReleaseGpio(int gpio, const char* owner) {
28
+ /* A stale release from one module must not free another module's claim. */
29
+ if (owner && mik__gpio_tracked(gpio) && s_owner[gpio] && strcmp(s_owner[gpio], owner) == 0) {
30
+ s_owner[gpio] = nullptr;
31
+ }
32
+ }
33
+
34
+ const char* MIK_GpioOwner(int gpio) {
35
+ return mik__gpio_tracked(gpio) ? s_owner[gpio] : nullptr;
36
+ }
37
+
38
+ /* Owners are class names ("by Pwm"), except the console ("by the console"). */
39
+ static std::string mik__gpio_in_use_message(int gpio, const char* owner) {
40
+ std::string message = "GPIO " + std::to_string(gpio) + " is already in use by ";
41
+ if (strcmp(owner, "console") == 0) message += "the ";
42
+ return message + owner;
43
+ }
44
+
45
+ JSValue mik__claim_gpios(JSContext* ctx, const int* gpios, int count, const char* owner) {
46
+ for (int i = 0; i < count; i++) {
47
+ if (MIK_ClaimGpio(gpios[i], owner)) continue;
48
+ const char* holder = MIK_GpioOwner(gpios[i]);
49
+ JSValue error = JS_NewObject(ctx);
50
+ JS_SetPropertyStr(ctx, error, "name", JS_NewString(ctx, "GpioInUse"));
51
+ JS_SetPropertyStr(ctx, error, "owner", JS_NewString(ctx, holder));
52
+ std::string message = mik__gpio_in_use_message(gpios[i], holder);
53
+ JS_SetPropertyStr(ctx, error, "message",
54
+ JS_NewStringLen(ctx, message.data(), message.size()));
55
+ mik__release_gpios(gpios, i, owner);
56
+ return mik__result_err_obj(ctx, error);
57
+ }
58
+ return JS_UNDEFINED;
59
+ }
60
+
61
+ void mik__release_gpios(const int* gpios, int count, const char* owner) {
62
+ for (int i = 0; i < count; i++) MIK_ReleaseGpio(gpios[i], owner);
63
+ }
package/src/mik_abort.cpp CHANGED
@@ -52,8 +52,7 @@ static JSValue mik__abort_lazy_get(JSContext* ctx, JSValue this_val, int magic)
52
52
  * would surface a bare 'null'. Mirrors the module loader's
53
53
  * missing-builtin fallback. */
54
54
  if (!JS_HasException(ctx)) {
55
- JS_ThrowReferenceError(ctx,
56
- "Builtin module 'mikro/abort' is not available in this build");
55
+ JS_ThrowTypeError(ctx, "Failed to resolve module specifier 'mikro/abort'");
57
56
  }
58
57
  JS_FreeValue(ctx, global_obj);
59
58
  return JS_EXCEPTION;
@@ -3,7 +3,6 @@
3
3
  #include <cstring>
4
4
  #include <string>
5
5
  #include <unistd.h>
6
- #include <vector>
7
6
 
8
7
  #include <quickjs.h>
9
8
 
@@ -122,41 +121,6 @@ static std::string mik__format(JSContext* ctx, int argc, JSValue* argv, bool col
122
121
  return result;
123
122
  }
124
123
 
125
- /* ── Error formatting (port of console.ts formatError) ───────────── */
126
-
127
- static std::string mik__format_error(JSContext* ctx, JSValue error) {
128
- JSValue stack_val = JS_GetPropertyStr(ctx, error, "stack");
129
- JSValue msg_val = JS_GetPropertyStr(ctx, error, "message");
130
-
131
- const char* stack = JS_ToCString(ctx, stack_val);
132
- const char* msg = JS_ToCString(ctx, msg_val);
133
-
134
- std::string result;
135
- if (stack && msg && strstr(stack, msg)) {
136
- /* Stack already includes message */
137
- result = stack;
138
- } else {
139
- JSValue name_val = JS_GetPropertyStr(ctx, error, "name");
140
- const char* name = JS_ToCString(ctx, name_val);
141
- result = name ? name : "Error";
142
- result += ": ";
143
- result += msg ? msg : "";
144
- if (stack) {
145
- result += "\n";
146
- result += stack;
147
- }
148
- if (name) JS_FreeCString(ctx, name);
149
- JS_FreeValue(ctx, name_val);
150
- }
151
-
152
- if (stack) JS_FreeCString(ctx, stack);
153
- if (msg) JS_FreeCString(ctx, msg);
154
- JS_FreeValue(ctx, stack_val);
155
- JS_FreeValue(ctx, msg_val);
156
-
157
- return result;
158
- }
159
-
160
124
  /* ── Console methods ─────────────────────────────────────────────── */
161
125
 
162
126
  static JSValue mik__console_log(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
@@ -205,24 +169,9 @@ static JSValue mik__console_error_warn(JSContext* ctx, JSValue this_val, int arg
205
169
  int magic) {
206
170
  uint8_t msg_type = magic == 0 ? MIK_MSG_ERROR : MIK_MSG_WARN;
207
171
 
208
- /* Map Error objects to their stack traces */
209
- std::vector<JSValue> mapped;
210
- mapped.reserve(argc);
211
-
212
- for (int i = 0; i < argc; i++) {
213
- if (JS_IsError(argv[i])) {
214
- std::string err_str = mik__format_error(ctx, argv[i]);
215
- mapped.push_back(JS_NewString(ctx, err_str.c_str()));
216
- } else {
217
- mapped.push_back(JS_DupValue(ctx, argv[i]));
218
- }
219
- }
220
-
221
- std::string output = mik__format(ctx, argc, mapped.data(), true);
222
-
223
- for (auto& v : mapped) {
224
- JS_FreeValue(ctx, v);
225
- }
172
+ /* Error arguments render through inspect: name, message, extra fields,
173
+ * stack and cause chain. Same output as console.log for the same value. */
174
+ std::string output = mik__format(ctx, argc, argv, true);
226
175
 
227
176
  if (mik__repl_is_protocol_mode()) {
228
177
  mik__repl_proto_send_output(msg_type, output.c_str(), output.size());
@@ -236,6 +185,152 @@ static JSValue mik__console_error_warn(JSContext* ctx, JSValue this_val, int arg
236
185
 
237
186
  /* ── Uncaught error reporting ─────────────────────────────────────── */
238
187
 
188
+ /* Bounded stack-buffer writer for the uncaught path: never touches the
189
+ * C++ heap (see mik__report_uncaught). Silently truncates when full. */
190
+ struct MIKBoundedBuf {
191
+ char* buf;
192
+ size_t cap;
193
+ size_t pos = 0;
194
+ void append(const char* s, size_t len) {
195
+ if (pos >= cap - 1) return;
196
+ size_t avail = cap - 1 - pos;
197
+ size_t n = len < avail ? len : avail;
198
+ memcpy(buf + pos, s, n);
199
+ pos += n;
200
+ }
201
+ void cstr(const char* s) {
202
+ if (s) append(s, strlen(s));
203
+ }
204
+ };
205
+
206
+ /* One primitive as a log token (strings quoted). Objects print as
207
+ * [object]: nested inspection would need the C++ heap. */
208
+ static void mik__append_primitive(JSContext* ctx, JSValue v, MIKBoundedBuf& out) {
209
+ if (JS_IsObject(v)) {
210
+ out.cstr("[object]");
211
+ return;
212
+ }
213
+ /* Allocation-free spellings first: a null rejection is how QuickJS
214
+ * reports OOM, so this must work with no heap at all */
215
+ if (JS_IsNull(v)) {
216
+ out.cstr("null");
217
+ return;
218
+ }
219
+ if (JS_IsUndefined(v)) {
220
+ out.cstr("undefined");
221
+ return;
222
+ }
223
+ if (JS_IsBool(v)) {
224
+ out.cstr(JS_ToBool(ctx, v) ? "true" : "false");
225
+ return;
226
+ }
227
+ if (JS_VALUE_GET_TAG(v) == JS_TAG_INT) {
228
+ char num[16];
229
+ int n = snprintf(num, sizeof(num), "%d", JS_VALUE_GET_INT(v));
230
+ if (n > 0) out.append(num, (size_t)n);
231
+ return;
232
+ }
233
+ bool quote = JS_IsString(v);
234
+ /* JS-heap allocation only; returns NULL on OOM instead of throwing */
235
+ const char* s = JS_ToCString(ctx, v);
236
+ if (!s) {
237
+ /* Symbols reject ToString */
238
+ JS_FreeValue(ctx, JS_GetException(ctx));
239
+ out.cstr("[primitive]");
240
+ return;
241
+ }
242
+ if (quote) out.cstr("'");
243
+ out.cstr(s);
244
+ if (quote) out.cstr("'");
245
+ JS_FreeCString(ctx, s);
246
+ }
247
+
248
+ /* "Name: message { extra: fields }" then the stack frames, `indent`
249
+ * prefixed to every frame line. Also fits a plain object (a Result error
250
+ * as a panic cause has name/message and no stack). */
251
+ static void mik__append_error_head(JSContext* ctx, JSValue exc, const char* indent, MIKBoundedBuf& out) {
252
+ JSValue name_val = JS_GetPropertyStr(ctx, exc, "name");
253
+ JSValue msg_val = JS_GetPropertyStr(ctx, exc, "message");
254
+ if (JS_IsException(name_val) || JS_IsException(msg_val)) {
255
+ JS_FreeValue(ctx, JS_GetException(ctx));
256
+ }
257
+ /* Non-string name/message count as absent (a native Result error has
258
+ * code + message and no name) */
259
+ const char* name = JS_IsString(name_val) ? JS_ToCString(ctx, name_val) : nullptr;
260
+ const char* emsg = JS_IsString(msg_val) ? JS_ToCString(ctx, msg_val) : nullptr;
261
+
262
+ if (name && name[0]) {
263
+ out.cstr(name);
264
+ if (emsg && emsg[0]) {
265
+ out.cstr(": ");
266
+ out.cstr(emsg);
267
+ }
268
+ } else if (emsg && emsg[0]) {
269
+ out.cstr(emsg);
270
+ } else {
271
+ out.cstr("[object]");
272
+ }
273
+
274
+ if (name) JS_FreeCString(ctx, name);
275
+ if (emsg) JS_FreeCString(ctx, emsg);
276
+ JS_FreeValue(ctx, name_val);
277
+ JS_FreeValue(ctx, msg_val);
278
+
279
+ /* Own enumerable fields beyond the standard ones: code, errno, path... */
280
+ JSPropertyEnum* ptab = nullptr;
281
+ uint32_t plen = 0;
282
+ if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, exc, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) ==
283
+ 0) {
284
+ bool first = true;
285
+ for (uint32_t i = 0; i < plen; i++) {
286
+ const char* key = JS_AtomToCString(ctx, ptab[i].atom);
287
+ if (key && strcmp(key, "name") != 0 && strcmp(key, "message") != 0 &&
288
+ strcmp(key, "stack") != 0 && strcmp(key, "cause") != 0) {
289
+ JSValue v = JS_GetProperty(ctx, exc, ptab[i].atom);
290
+ if (JS_IsException(v)) {
291
+ JS_FreeValue(ctx, JS_GetException(ctx));
292
+ } else {
293
+ out.cstr(first ? " { " : ", ");
294
+ first = false;
295
+ out.cstr(key);
296
+ out.cstr(": ");
297
+ mik__append_primitive(ctx, v, out);
298
+ }
299
+ JS_FreeValue(ctx, v);
300
+ }
301
+ if (key) JS_FreeCString(ctx, key);
302
+ JS_FreeAtom(ctx, ptab[i].atom);
303
+ }
304
+ if (!first) out.cstr(" }");
305
+ js_free(ctx, ptab);
306
+ } else {
307
+ JS_FreeValue(ctx, JS_GetException(ctx));
308
+ }
309
+
310
+ JSValue stack_val = JS_GetPropertyStr(ctx, exc, "stack");
311
+ if (JS_IsString(stack_val)) {
312
+ const char* stack = JS_ToCString(ctx, stack_val);
313
+ if (stack) {
314
+ const char* p = stack;
315
+ while (*p) {
316
+ const char* nl = strchr(p, '\n');
317
+ size_t len = nl ? (size_t)(nl - p) : strlen(p);
318
+ if (len) {
319
+ out.cstr("\n");
320
+ out.cstr(indent);
321
+ out.append(p, len);
322
+ }
323
+ if (!nl) break;
324
+ p = nl + 1;
325
+ }
326
+ JS_FreeCString(ctx, stack);
327
+ }
328
+ } else if (JS_IsException(stack_val)) {
329
+ JS_FreeValue(ctx, JS_GetException(ctx));
330
+ }
331
+ JS_FreeValue(ctx, stack_val);
332
+ }
333
+
239
334
  /* Dedup across report paths: one error can reach this function more than
240
335
  * once for a single failure. The sync throw path (mik_dump_error) can report
241
336
  * an error whose promise still sits in the deferred flush queue, e.g. an
@@ -275,77 +370,59 @@ bool mik__report_uncaught(JSContext* ctx, JSValue exc, bool in_promise) {
275
370
  * abort() — masking the real OOM the reporter was trying to surface.
276
371
  * Messages longer than the buffer get truncated, which beats
277
372
  * aborting. */
278
- char buf[512];
279
- size_t pos = 0;
280
- auto append = [&](const char* s, size_t len) {
281
- if (pos >= sizeof(buf) - 1) return;
282
- size_t avail = sizeof(buf) - 1 - pos;
283
- size_t n = len < avail ? len : avail;
284
- memcpy(buf + pos, s, n);
285
- pos += n;
286
- };
287
- auto append_cstr = [&](const char* s) {
288
- if (s) append(s, strlen(s));
289
- };
373
+ char buf[1024];
374
+ MIKBoundedBuf out{buf, sizeof(buf)};
290
375
 
291
- append_cstr(in_promise ? "Uncaught (in promise) " : "Uncaught ");
376
+ out.cstr(in_promise ? "Uncaught (in promise) " : "Uncaught ");
292
377
 
293
378
  if (JS_IsObject(exc)) {
294
- JSValue name_val = JS_GetPropertyStr(ctx, exc, "name");
295
- JSValue msg_val = JS_GetPropertyStr(ctx, exc, "message");
296
- const char* name = JS_ToCString(ctx, name_val);
297
- const char* emsg = JS_ToCString(ctx, msg_val);
298
-
299
- if (name && name[0]) {
300
- append_cstr(name);
301
- if (emsg && emsg[0]) {
302
- append_cstr(": ");
303
- append_cstr(emsg);
379
+ mik__append_error_head(ctx, exc, "", out);
380
+
381
+ /* Cause chain: a panic's cause is the Result error that triggered
382
+ * it, so this is usually the line that says what actually failed.
383
+ * Bounded depth; `seen` cuts cycles. */
384
+ static const char* const indents[] = {"", " ", " ", " ", " "};
385
+ void* seen[countof(indents)] = {JS_VALUE_GET_PTR(exc)};
386
+ JSValue cur = JS_DupValue(ctx, exc);
387
+ for (size_t level = 1; level < countof(indents); level++) {
388
+ JSValue cause = JS_GetPropertyStr(ctx, cur, "cause");
389
+ JS_FreeValue(ctx, cur);
390
+ cur = cause;
391
+ if (JS_IsException(cause)) {
392
+ JS_FreeValue(ctx, JS_GetException(ctx));
393
+ break;
304
394
  }
305
- } else if (emsg && emsg[0]) {
306
- append_cstr(emsg);
307
- } else {
308
- append_cstr("[object]");
309
- }
310
-
311
- if (name) JS_FreeCString(ctx, name);
312
- if (emsg) JS_FreeCString(ctx, emsg);
313
- JS_FreeValue(ctx, name_val);
314
- JS_FreeValue(ctx, msg_val);
315
-
316
- JSValue stack_val = JS_GetPropertyStr(ctx, exc, "stack");
317
- if (JS_IsString(stack_val)) {
318
- const char* stack = JS_ToCString(ctx, stack_val);
319
- if (stack && stack[0]) {
320
- append_cstr("\n");
321
- append_cstr(stack);
395
+ if (JS_IsUndefined(cause)) break;
396
+ out.cstr("\n");
397
+ out.cstr(indents[level]);
398
+ out.cstr("[cause]: ");
399
+ if (!JS_IsObject(cause)) {
400
+ mik__append_primitive(ctx, cause, out);
401
+ break;
402
+ }
403
+ bool cyclic = false;
404
+ for (size_t j = 0; j < level; j++) {
405
+ if (seen[j] == JS_VALUE_GET_PTR(cause)) cyclic = true;
406
+ }
407
+ if (cyclic) {
408
+ out.cstr("[Circular]");
409
+ break;
322
410
  }
323
- if (stack) JS_FreeCString(ctx, stack);
411
+ seen[level] = JS_VALUE_GET_PTR(cause);
412
+ mik__append_error_head(ctx, cause, indents[level], out);
324
413
  }
325
- JS_FreeValue(ctx, stack_val);
414
+ JS_FreeValue(ctx, cur);
326
415
  } else {
327
416
  /* Describe primitive rejections by type without going through
328
417
  * mik_inspect (which allocates a std::string). */
329
- if (JS_IsNull(exc)) {
330
- append_cstr("null");
331
- } else if (JS_IsUndefined(exc)) {
332
- append_cstr("undefined");
333
- } else if (JS_IsBool(exc)) {
334
- append_cstr(JS_ToBool(ctx, exc) ? "true" : "false");
335
- } else if (JS_IsNumber(exc)) {
336
- double d = 0;
337
- JS_ToFloat64(ctx, &d, exc);
338
- char num[32];
339
- int n = snprintf(num, sizeof(num), "%g", d);
340
- if (n > 0) append(num, (size_t)n);
341
- } else if (JS_IsString(exc)) {
418
+ if (JS_IsString(exc)) {
342
419
  const char* s = JS_ToCString(ctx, exc);
343
420
  if (s) {
344
- append_cstr(s);
421
+ out.cstr(s);
345
422
  JS_FreeCString(ctx, s);
346
423
  }
347
424
  } else {
348
- append_cstr("[primitive]");
425
+ mik__append_primitive(ctx, exc, out);
349
426
  }
350
427
 
351
428
  /* Primitive rejections lose the throw-site stack (the async
@@ -359,17 +436,17 @@ bool mik__report_uncaught(JSContext* ctx, JSValue exc, bool in_promise) {
359
436
  int n = snprintf(note, sizeof(note),
360
437
  "\n (primitive rejection, systemFree=%zuB at report time)",
361
438
  plat->get_free_system_mem());
362
- if (n > 0) append(note, (size_t)n);
439
+ if (n > 0) out.append(note, (size_t)n);
363
440
  }
364
441
  }
365
442
 
366
443
  if (mik__repl_is_protocol_mode()) {
367
- mik__repl_proto_send_output(MIK_MSG_ERROR, buf, pos);
444
+ mik__repl_proto_send_output(MIK_MSG_ERROR, buf, out.pos);
368
445
  return true;
369
446
  }
370
447
 
371
- append_cstr("\r\n");
372
- MIK_GetPlatform()->stderr_write(buf, pos);
448
+ out.cstr("\r\n");
449
+ MIK_GetPlatform()->stderr_write(buf, out.pos);
373
450
  return true;
374
451
  }
375
452
 
@@ -50,20 +50,9 @@ JSValue hc_iter_result(JSContext* ctx, bool done, JSValue value) {
50
50
  return o;
51
51
  }
52
52
 
53
- JSAtom hc_async_iterator_atom(JSContext* ctx) {
54
- JSValue g = JS_GetGlobalObject(ctx);
55
- JSValue sym_ctor = JS_GetPropertyStr(ctx, g, "Symbol");
56
- JSValue sym = JS_GetPropertyStr(ctx, sym_ctor, "asyncIterator");
57
- JSAtom atom = JS_ValueToAtom(ctx, sym);
58
- JS_FreeValue(ctx, sym);
59
- JS_FreeValue(ctx, sym_ctor);
60
- JS_FreeValue(ctx, g);
61
- return atom;
62
- }
63
-
64
53
  /* source[Symbol.asyncIterator]() — returns the iterator (owned) or exception. */
65
54
  JSValue hc_get_async_iterator(JSContext* ctx, JSValue source) {
66
- JSAtom atom = hc_async_iterator_atom(ctx);
55
+ JSAtom atom = mik__async_iterator_atom(ctx);
67
56
  JSValue fn = JS_GetProperty(ctx, source, atom);
68
57
  JS_FreeAtom(ctx, atom);
69
58
  if (JS_IsException(fn)) return fn;
@@ -757,7 +746,7 @@ JSValue hc_make_response(JSContext* ctx, JSValue raw) {
757
746
  JS_PROP_C_W_E);
758
747
 
759
748
  JSValue body = JS_NewObject(ctx);
760
- JSAtom iter_atom = hc_async_iterator_atom(ctx);
749
+ JSAtom iter_atom = mik__async_iterator_atom(ctx);
761
750
  JS_DefinePropertyValue(ctx, body, iter_atom,
762
751
  JS_NewCFunctionData(ctx, hc_resp_body_iterator_cf, 0, 0, 2, data),
763
752
  JS_PROP_C_W_E);
@@ -1093,7 +1082,7 @@ JSValue hc_on_headers(JSContext* ctx, JSValue this_val, int argc, JSValue* argv,
1093
1082
  }
1094
1083
 
1095
1084
  JSValue raw_body = JS_NewObject(ctx);
1096
- JSAtom iter_atom = hc_async_iterator_atom(ctx);
1085
+ JSAtom iter_atom = mik__async_iterator_atom(ctx);
1097
1086
  JS_DefinePropertyValue(ctx, raw_body, iter_atom,
1098
1087
  JS_NewCFunctionData(ctx, hc_transport_iterator_cf, 0, 0, 1, &st),
1099
1088
  JS_PROP_C_W_E);
@@ -18,6 +18,8 @@ struct InspectOpts {
18
18
  bool colors;
19
19
  bool show_hidden;
20
20
  std::vector<void*> seen;
21
+ /* Nesting of [cause] rendering, capped like mik__report_uncaught */
22
+ int cause_level = 0;
21
23
  };
22
24
 
23
25
  /* ── Helpers ─────────────────────────────────────────────────────── */
@@ -85,6 +87,7 @@ static std::string quote_key(const char* key) {
85
87
  /* ── Forward declaration ─────────────────────────────────────────── */
86
88
 
87
89
  static std::string inspect_value(JSContext* ctx, JSValue value, InspectOpts& opts, int depth);
90
+ static std::string indent_continuation(const std::string& s);
88
91
 
89
92
  /* ── Custom inspect callback state (single-threaded JS) ──────────── */
90
93
 
@@ -263,7 +266,7 @@ static std::string inspect_array_items(JSContext* ctx, JSValue arr, uint32_t len
263
266
  if (JS_IsException(item)) {
264
267
  out += getter_threw(ctx, opts);
265
268
  } else {
266
- out += inspect_value(ctx, item, opts, depth);
269
+ out += indent_continuation(inspect_value(ctx, item, opts, depth));
267
270
  }
268
271
  JS_FreeValue(ctx, item);
269
272
  }
@@ -318,7 +321,7 @@ static std::string inspect_properties(JSContext* ctx, JSValue obj, InspectOpts&
318
321
  if (JS_IsException(val)) {
319
322
  out += getter_threw(ctx, opts);
320
323
  } else {
321
- out += inspect_value(ctx, val, opts, depth);
324
+ out += indent_continuation(inspect_value(ctx, val, opts, depth));
322
325
  }
323
326
  JS_FreeValue(ctx, val);
324
327
  }
@@ -474,6 +477,51 @@ static std::string inspect_regexp(JSContext* ctx, JSValue value, InspectOpts& op
474
477
  return stylize("/(?:)/", MIK_TOKEN_REGEXP, opts);
475
478
  }
476
479
 
480
+ /* Indent every line after the first by two spaces so a multi-line value
481
+ * (an error's stack frames, its own cause) nests visibly under its parent. */
482
+ static std::string indent_continuation(const std::string& s) {
483
+ std::string out;
484
+ out.reserve(s.size() + 8);
485
+ for (char c : s) {
486
+ out += c;
487
+ if (c == '\n') out += " ";
488
+ }
489
+ return out;
490
+ }
491
+
492
+ /* "\n [cause]: <value>" for an own cause, or "". Shared by Errors and
493
+ * plain objects, since Result errors carry cause as an ordinary field.
494
+ * The chain is what a log line exists for, so it renders past the depth
495
+ * limit; the caller keeps the parent on the seen list to bound cycles. */
496
+ static std::string inspect_cause(JSContext* ctx, JSValue value, InspectOpts& opts, int depth) {
497
+ JSValue cause = JS_GetPropertyStr(ctx, value, "cause");
498
+ if (JS_IsException(cause)) {
499
+ drain_exception(ctx);
500
+ return "";
501
+ }
502
+ /* Same cap as mik__report_uncaught; bounds recursion on the JS task stack */
503
+ static const int MAX_CAUSE_LEVEL = 4;
504
+ std::string out;
505
+ if (!JS_IsUndefined(cause)) {
506
+ out = "\n [cause]: ";
507
+ if (opts.cause_level >= MAX_CAUSE_LEVEL) {
508
+ out += TRUNCATOR;
509
+ } else {
510
+ opts.cause_level++;
511
+ out += indent_continuation(inspect_value(ctx, cause, opts, depth < 1 ? 1 : depth));
512
+ opts.cause_level--;
513
+ }
514
+ }
515
+ JS_FreeValue(ctx, cause);
516
+ return out;
517
+ }
518
+
519
+ /* Errors render as
520
+ * Name: message { extra: props }
521
+ * at frame (file:line:col)
522
+ * [cause]: <inspected cause>
523
+ * The header keeps name, message and every own enumerable field (code,
524
+ * errno, ...) on one line; the stack and the cause chain follow. */
477
525
  static std::string inspect_error(JSContext* ctx, JSValue value, InspectOpts& opts, int depth) {
478
526
  /* Get name and message */
479
527
  JSValue name_val = JS_GetPropertyStr(ctx, value, "name");
@@ -498,16 +546,40 @@ static std::string inspect_error(JSContext* ctx, JSValue value, InspectOpts& opt
498
546
  JS_FreeValue(ctx, name_val);
499
547
  JS_FreeValue(ctx, msg_val);
500
548
 
501
- /* Inspect extra properties (skip standard error keys) */
549
+ if (is_seen(opts, value)) return result + " [Circular]";
550
+ opts.seen.push_back(JS_VALUE_GET_PTR(value));
551
+
552
+ /* Inspect extra properties (skip standard error keys; cause is rendered
553
+ * below so it reads the same whether QuickJS or a panic defined it) */
502
554
  static const char* const error_keys[] = {"stack", "line", "column", "name",
503
555
  "message", "fileName", "lineNumber", "columnNumber",
504
- "number", "description"};
556
+ "number", "description", "cause"};
505
557
  std::string props =
506
558
  inspect_properties(ctx, value, opts, depth, error_keys, countof(error_keys));
507
559
  if (!props.empty()) {
508
560
  result += " { " + props + " }";
509
561
  }
510
562
 
563
+ /* QuickJS stacks are frame lines only (" at ..."), no header line */
564
+ JSValue stack_val = JS_GetPropertyStr(ctx, value, "stack");
565
+ if (JS_IsException(stack_val)) drain_exception(ctx);
566
+ if (JS_IsString(stack_val)) {
567
+ const char* stack = JS_ToCString(ctx, stack_val);
568
+ if (stack) {
569
+ std::string frames(stack);
570
+ JS_FreeCString(ctx, stack);
571
+ while (!frames.empty() && frames.back() == '\n') frames.pop_back();
572
+ if (!frames.empty()) {
573
+ result += "\n";
574
+ result += frames;
575
+ }
576
+ }
577
+ }
578
+ JS_FreeValue(ctx, stack_val);
579
+
580
+ result += inspect_cause(ctx, value, opts, depth);
581
+
582
+ opts.seen.pop_back();
511
583
  return result;
512
584
  }
513
585
 
@@ -603,9 +675,9 @@ static std::string inspect_map(JSContext* ctx, JSValue value, InspectOpts& opts,
603
675
 
604
676
  if (!first) items += ", ";
605
677
  first = false;
606
- items += inspect_value(ctx, key, opts, depth);
678
+ items += indent_continuation(inspect_value(ctx, key, opts, depth));
607
679
  items += " => ";
608
- items += inspect_value(ctx, val, opts, depth);
680
+ items += indent_continuation(inspect_value(ctx, val, opts, depth));
609
681
 
610
682
  JS_FreeValue(ctx, key);
611
683
  JS_FreeValue(ctx, val);
@@ -673,7 +745,7 @@ static std::string inspect_set(JSContext* ctx, JSValue value, InspectOpts& opts,
673
745
  JSValue val = JS_GetPropertyStr(ctx, step, "value");
674
746
  if (!first) items += ", ";
675
747
  first = false;
676
- items += inspect_value(ctx, val, opts, depth);
748
+ items += indent_continuation(inspect_value(ctx, val, opts, depth));
677
749
  JS_FreeValue(ctx, val);
678
750
  JS_FreeValue(ctx, step);
679
751
  }
@@ -746,11 +818,16 @@ static std::string inspect_object(JSContext* ctx, JSValue value, InspectOpts& op
746
818
 
747
819
  if (plen == 0) return "{}";
748
820
 
821
+ /* A cause field chains below the braces like an Error's, so a Result
822
+ * error wrapped around another keeps its whole chain readable */
823
+ static const char* const cause_key[] = {"cause"};
749
824
  opts.seen.push_back(JS_VALUE_GET_PTR(value));
750
- std::string props = inspect_properties(ctx, value, opts, depth);
825
+ std::string props = inspect_properties(ctx, value, opts, depth, cause_key, 1);
826
+ std::string result = props.empty() ? "{}" : "{ " + props + " }";
827
+ result += inspect_cause(ctx, value, opts, depth);
751
828
  opts.seen.pop_back();
752
829
 
753
- return "{ " + props + " }";
830
+ return result;
754
831
  }
755
832
 
756
833
  static std::string inspect_class(JSContext* ctx, JSValue value, const std::string& type_name,