@mikrojs/native 0.12.0 → 0.14.0-pr-229.g0d8db1b

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.
@@ -34,7 +34,7 @@ declare module 'native:observable' {
34
34
  }
35
35
 
36
36
  declare module 'native:sys' {
37
- import type {JsMemoryUsage} from './sys/types.js'
37
+ import type {JsMemoryUsage, ResetReason} from './sys/types.js'
38
38
  export function evalScript(code: string): Promise<{value: unknown}>
39
39
  export function memoryUsage(): {
40
40
  heapUsed: number
@@ -67,6 +67,7 @@ declare module 'native:sys' {
67
67
  }
68
68
  export const firmware: {hash: string; date: string; idfVersion: string | undefined}
69
69
  export const deviceId: string
70
+ export const resetReason: ResetReason
70
71
  }
71
72
 
72
73
  declare module 'native:fs' {
@@ -37,6 +37,8 @@ export const firmware = native.firmware
37
37
 
38
38
  export const deviceId: string = native.deviceId
39
39
 
40
+ export const resetReason = native.resetReason
41
+
40
42
  export function restart(): never {
41
43
  return native.restart() as never
42
44
  }
@@ -73,7 +73,10 @@ export interface JsMemoryUsage {
73
73
  arrayCount: number
74
74
  /** Total element slots across all fast arrays */
75
75
  fastArrayElements: number
76
- /** Bytes of backing storage for ArrayBuffer / TypedArray data */
76
+ /** Cumulative bytes deserialized via JS_ReadObject over this
77
+ * runtime's lifetime (bytecode of imported modules, builtins, and
78
+ * bjson). A monotonic counter, NOT live memory — it never goes
79
+ * down, so don't read it as retained ArrayBuffer storage. */
77
80
  binaryObjectSize: number
78
81
  }
79
82
 
@@ -122,6 +125,29 @@ export declare const firmware: {
122
125
  readonly idfVersion: string | undefined
123
126
  }
124
127
 
128
+ /** Why the device last reset. A clean `restart()` reports `'software'`;
129
+ * a crash reports `'panic'`. Host/Node builds always report `'unknown'`. */
130
+ export type ResetReason =
131
+ | 'power-on'
132
+ | 'software'
133
+ | 'panic'
134
+ | 'watchdog'
135
+ | 'interrupt-watchdog'
136
+ | 'task-watchdog'
137
+ | 'brownout'
138
+ | 'deep-sleep'
139
+ | 'external'
140
+ | 'sdio'
141
+ | 'usb'
142
+ | 'jtag'
143
+ | 'efuse'
144
+ | 'power-glitch'
145
+ | 'cpu-lockup'
146
+ | 'unknown'
147
+
148
+ /** Reason the device last reset, determined once at boot. */
149
+ export declare const resetReason: ResetReason
150
+
125
151
  export declare function restart(): never
126
152
 
127
153
  /** Reason the device woke up this boot. Set after deep sleep (or first
package/src/builtins.cpp CHANGED
@@ -32,11 +32,16 @@ static JSModuleDef* mik__deserialize_builtin(JSContext* ctx, const char* name,
32
32
  JSValue obj = JS_ReadObject(ctx, data, data_size, JS_READ_OBJ_BYTECODE);
33
33
 
34
34
  if (JS_IsException(obj)) {
35
- platform->log(MIK_LOG_ERROR, "mikrojs", "Failed to deserialize bytecode for builtin '%s' (%u bytes)", name,
36
- data_size);
37
- /* Leave the JS exception on ctx so the caller can propagate the
38
- * real error (e.g. "Maximum call stack size exceeded") instead of
39
- * replacing it with a generic "not available" message. */
35
+ /* Peek the exception for the log, then re-throw it so the caller
36
+ * can still propagate the real error (e.g. "Maximum call stack
37
+ * size exceeded") instead of a generic "not available" message. */
38
+ JSValue exc = JS_GetException(ctx);
39
+ const char* msg = JS_ToCString(ctx, exc);
40
+ platform->log(MIK_LOG_ERROR, "mikrojs",
41
+ "Failed to deserialize bytecode for builtin '%s' (%u bytes): %s", name,
42
+ data_size, msg ? msg : "unknown error");
43
+ if (msg) JS_FreeCString(ctx, msg);
44
+ JS_Throw(ctx, exc);
40
45
  return NULL;
41
46
  }
42
47
 
package/src/mik_abort.cpp CHANGED
@@ -1,110 +1,32 @@
1
1
  #include <quickjs.h>
2
2
 
3
+ #include "mikrojs/private.h"
3
4
  #include "mikrojs/utils.h"
4
5
 
5
- /* Minimal AbortController / AbortSignal implementation.
6
+ /* AbortController / AbortSignal globals.
6
7
  *
7
- * This is a lightweight subset of the WHATWG DOM spec — just enough
8
- * for fetch timeouts and cooperative cancellation. No EventTarget
9
- * dependency: listeners are stored in a simple array.
8
+ * The implementation lives in runtime/abort/abort.ts and ships as
9
+ * precompiled bytecode in the builtins table ("mikro/abort").
10
+ * Evaluating that module installs AbortController, AbortSignal,
11
+ * AbortError, and TimeoutError on globalThis.
10
12
  *
11
- * Implements:
12
- * AbortSignal: aborted, reason, throwIfAborted(), onabort,
13
- * addEventListener("abort", fn), removeEventListener("abort", fn)
14
- * AbortSignal.abort(reason) pre-aborted signal
15
- * AbortSignal.timeout(ms) — auto-aborts after delay
16
- * AbortSignal.any(signals) — aborts when any input signal aborts
17
- * AbortController: signal, abort(reason)
18
- *
19
- * Lazy-initialized: the JS class hierarchy is compiled and installed
20
- * on first access to any of the four globals (AbortController,
21
- * AbortSignal, AbortError, TimeoutError).
13
+ * Lazy-initialized: this file installs lazy getters for the four
14
+ * globals; the first access evaluates the bytecode module. Bytecode
15
+ * (rather than JS source evaluated at runtime) keeps the QuickJS
16
+ * parser out of the install path the parse peak pushed low-memory
17
+ * chips into OOM when the heap was already near full.
22
18
  */
23
19
 
24
- static const char abort_js[] = R"JS(
25
- (function(g) {
26
- "use strict";
27
-
28
- class AbortError extends Error {
29
- constructor(message) { super(message || "The operation was aborted"); }
30
- get name() { return "AbortError"; }
31
- }
32
- class TimeoutError extends Error {
33
- constructor(message) { super(message || "The operation timed out"); }
34
- get name() { return "TimeoutError"; }
35
- }
36
-
37
- const _a = Symbol(), _r = Symbol(), _l = Symbol(), _s = Symbol();
38
- const ABORT_REASON = () => new AbortError();
39
-
40
- function doAbort(signal, reason) {
41
- if (signal[_a]) return;
42
- signal[_a] = true;
43
- signal[_r] = reason;
44
- if (typeof signal.onabort === "function") signal.onabort();
45
- for (const fn of signal[_l]) fn();
46
- }
47
-
48
- class AbortSignal {
49
- constructor() {
50
- this[_a] = false;
51
- this[_r] = undefined;
52
- this[_l] = [];
53
- this.onabort = null;
54
- }
55
- get aborted() { return this[_a]; }
56
- get reason() { return this[_r]; }
57
- throwIfAborted() { if (this[_a]) throw this[_r]; }
58
- addEventListener(type, fn) {
59
- if (type === "abort" && typeof fn === "function") this[_l].push(fn);
60
- }
61
- removeEventListener(type, fn) {
62
- if (type === "abort") this[_l] = this[_l].filter(f => f !== fn);
63
- }
64
- static abort(reason) {
65
- const s = new AbortSignal();
66
- doAbort(s, reason !== undefined ? reason : ABORT_REASON());
67
- return s;
68
- }
69
- static timeout(ms) {
70
- const s = new AbortSignal();
71
- setTimeout(() => doAbort(s, new TimeoutError()), ms);
72
- return s;
73
- }
74
- static any(signals) {
75
- const s = new AbortSignal();
76
- for (const i of signals) {
77
- if (i.aborted) { doAbort(s, i.reason); return s; }
78
- }
79
- for (const i of signals) {
80
- i.addEventListener("abort", () => doAbort(s, i.reason));
81
- }
82
- return s;
83
- }
84
- }
85
-
86
- class AbortController {
87
- constructor() { this[_s] = new AbortSignal(); }
88
- get signal() { return this[_s]; }
89
- abort(reason) { doAbort(this[_s], reason !== undefined ? reason : ABORT_REASON()); }
90
- }
91
-
92
- g.AbortError = AbortError;
93
- g.TimeoutError = TimeoutError;
94
- g.AbortSignal = AbortSignal;
95
- g.AbortController = AbortController;
96
- })
97
- )JS";
98
-
99
- /* Names of the globals this module installs */
20
+ /* Names of the globals the abort module installs */
100
21
  static const char* const abort_global_names[] = {"AbortController", "AbortSignal", "AbortError",
101
22
  "TimeoutError"};
102
23
  static constexpr int ABORT_GLOBAL_COUNT = 4;
103
24
 
104
25
  /**
105
26
  * Lazy getter: on first access to any of the four globals, delete all
106
- * lazy getters, evaluate the JS to install real values, then return
107
- * the requested one. The magic parameter identifies which global.
27
+ * lazy getters, evaluate the bytecode module to install real values,
28
+ * then return the requested one. The magic parameter identifies which
29
+ * global.
108
30
  *
109
31
  * Signature must match JS_CFUNC_getter_magic: (ctx, this_val, magic).
110
32
  */
@@ -112,29 +34,40 @@ static JSValue mik__abort_lazy_get(JSContext* ctx, JSValue this_val, int magic)
112
34
 
113
35
  JSValue global_obj = JS_GetGlobalObject(ctx);
114
36
 
115
- /* Remove all four lazy getters so the JS assignment (g.X = Y) works */
37
+ /* Remove all four lazy getters so the module's globalThis
38
+ * assignments define plain properties */
116
39
  for (int i = 0; i < ABORT_GLOBAL_COUNT; i++) {
117
40
  JSAtom prop = JS_NewAtom(ctx, abort_global_names[i]);
118
41
  JS_DeleteProperty(ctx, global_obj, prop, 0);
119
42
  JS_FreeAtom(ctx, prop);
120
43
  }
121
44
 
122
- /* Evaluate and install the real globals */
123
- JSValue wrapper =
124
- JS_Eval(ctx, abort_js, sizeof(abort_js) - 1, "<abort>", JS_EVAL_TYPE_GLOBAL);
125
- if (JS_IsException(wrapper)) {
126
- JSValue exc = JS_GetException(ctx);
127
- JS_FreeValue(ctx, exc);
45
+ /* Evaluate the precompiled module; its side effect installs the
46
+ * globals. The module is synchronous, so the returned promise is
47
+ * already settled. */
48
+ JSModuleDef* m = mik__load_builtin(ctx, "mikro/abort");
49
+ if (m == NULL) {
50
+ /* A table miss returns NULL with no pending exception (only
51
+ * deserialize failures throw); without this guard the getter
52
+ * would surface a bare 'null'. Mirrors the module loader's
53
+ * missing-builtin fallback. */
54
+ if (!JS_HasException(ctx)) {
55
+ JS_ThrowReferenceError(ctx,
56
+ "Builtin module 'mikro/abort' is not available in this build");
57
+ }
128
58
  JS_FreeValue(ctx, global_obj);
129
- return JS_UNDEFINED;
59
+ return JS_EXCEPTION;
130
60
  }
131
- JSValue ret = JS_Call(ctx, wrapper, JS_UNDEFINED, 1, &global_obj);
132
- JS_FreeValue(ctx, wrapper);
61
+ JSValue ret = JS_EvalFunction(ctx, JS_DupValue(ctx, JS_MKPTR(JS_TAG_MODULE, m)));
133
62
  if (JS_IsException(ret)) {
134
- JSValue exc = JS_GetException(ctx);
135
- JS_FreeValue(ctx, exc);
136
63
  JS_FreeValue(ctx, global_obj);
137
- return JS_UNDEFINED;
64
+ return JS_EXCEPTION;
65
+ }
66
+ if (JS_PromiseState(ctx, ret) == JS_PROMISE_REJECTED) {
67
+ JSValue err = JS_PromiseResult(ctx, ret);
68
+ JS_FreeValue(ctx, ret);
69
+ JS_FreeValue(ctx, global_obj);
70
+ return JS_Throw(ctx, err);
138
71
  }
139
72
  JS_FreeValue(ctx, ret);
140
73
 
@@ -145,7 +78,7 @@ static JSValue mik__abort_lazy_get(JSContext* ctx, JSValue this_val, int magic)
145
78
  }
146
79
 
147
80
  void mik__abort_init(JSContext* ctx, JSValue global_obj) {
148
- /* Install lazy getters instead of eagerly evaluating the JS.
81
+ /* Install lazy getters instead of eagerly evaluating the module.
149
82
  * Each getter shares the same C function, distinguished by magic. */
150
83
  for (int i = 0; i < ABORT_GLOBAL_COUNT; i++) {
151
84
  JSAtom prop = JS_NewAtom(ctx, abort_global_names[i]);
@@ -236,31 +236,32 @@ static JSValue mik__console_error_warn(JSContext* ctx, JSValue this_val, int arg
236
236
 
237
237
  /* ── Uncaught error reporting ─────────────────────────────────────── */
238
238
 
239
- /* Dedup: the rejection tracker can fire multiple times for the same
240
- * error (e.g. inner Promise.reject + async wrapper rejection). Skip
241
- * if the same object pointer was just reported. */
242
- static void* last_reported_ptr = nullptr;
243
- static int64_t last_reported_time = 0;
244
-
245
- void mik__report_uncaught_reset(void) {
246
- last_reported_ptr = nullptr;
247
- }
248
-
249
- void mik__report_uncaught(JSContext* ctx, JSValue exc, bool in_promise) {
239
+ /* Dedup across report paths: one error can reach this function more than
240
+ * once for a single failure. A failed dynamic import leaves two never-handled
241
+ * promises with the SAME reason (QuickJS runs a module body as an async
242
+ * function and reads its rejected result by value, plus the rejection
243
+ * propagated up the await chain), and at the REPL the throw path reports an
244
+ * error the deferred flush then sees again. Mark the error object the first
245
+ * time it is reported and skip it afterwards. Keying on object identity is
246
+ * exact and time-independent; a recycled address belongs to a fresh object
247
+ * with no marker, so distinct errors are never suppressed. Primitive
248
+ * rejections (e.g. a null OOM rejection) can't hold a marker and are always
249
+ * reported. Returns true if it reported, false if it was a duplicate. */
250
+ bool mik__report_uncaught(JSContext* ctx, JSValue exc, bool in_promise) {
250
251
  if (JS_IsObject(exc)) {
251
- void* ptr = JS_VALUE_GET_PTR(exc);
252
- int64_t now = MIK_GetPlatform()->get_boot_us();
253
- /* Deduplicate only within 1ms the rejection tracker can fire
254
- * twice for the same error (inner + async wrapper) essentially
255
- * instantly, but a new error 1s later at a recycled address
256
- * must not be suppressed. */
257
- if (ptr == last_reported_ptr && (now - last_reported_time) < 1000) {
258
- return;
252
+ JSAtom marker = JS_NewAtom(ctx, "\xff" "mik_reported");
253
+ if (marker != JS_ATOM_NULL) {
254
+ if (JS_HasProperty(ctx, exc, marker) > 0) {
255
+ JS_FreeAtom(ctx, marker);
256
+ return false;
257
+ }
258
+ /* Non-enumerable, non-writable, non-configurable so it stays
259
+ * invisible to user code. Best-effort: JS_* calls return error
260
+ * codes (no C++ throw), so under OOM we just report without
261
+ * dedup rather than abort. */
262
+ JS_DefinePropertyValue(ctx, exc, marker, JS_TRUE, 0);
263
+ JS_FreeAtom(ctx, marker);
259
264
  }
260
- last_reported_ptr = ptr;
261
- last_reported_time = now;
262
- } else {
263
- last_reported_ptr = nullptr;
264
265
  }
265
266
 
266
267
  /* Fixed-size stack buffer so this function never allocates from the
@@ -360,11 +361,12 @@ void mik__report_uncaught(JSContext* ctx, JSValue exc, bool in_promise) {
360
361
 
361
362
  if (mik__repl_is_protocol_mode()) {
362
363
  mik__repl_proto_send_output(MIK_MSG_ERROR, buf, pos);
363
- return;
364
+ return true;
364
365
  }
365
366
 
366
367
  append_cstr("\r\n");
367
368
  MIK_GetPlatform()->stderr_write(buf, pos);
369
+ return true;
368
370
  }
369
371
 
370
372
  /* ── Test emit ────────────────────────────────────────────────────── */
@@ -805,7 +805,7 @@ std::string mik_inspect(JSContext* ctx, JSValue value, int depth, bool colors, b
805
805
  return inspect_value(ctx, value, opts, depth);
806
806
  }
807
807
 
808
- /* ── JS-callable inspect function (for mikrojs/inspect module) ───── */
808
+ /* ── JS-callable inspect function (for mikro/inspect module) ───── */
809
809
 
810
810
  static JSValue mik__inspect_js(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
811
811
  if (argc < 1) return JS_NewString(ctx, "undefined");
@@ -836,14 +836,14 @@ static JSValue mik__inspect_js(JSContext* ctx, JSValue this_val, int argc, JSVal
836
836
  return JS_NewString(ctx, result.c_str());
837
837
  }
838
838
 
839
- /* Module init for 'mikrojs/inspect' native C module */
839
+ /* Module init for 'mikro/inspect' native C module */
840
840
  int mik__inspect_module_init(JSContext* ctx, JSModuleDef* m) {
841
841
  return JS_SetModuleExport(ctx, m, "inspect",
842
842
  JS_NewCFunction(ctx, mik__inspect_js, "inspect", 1));
843
843
  }
844
844
 
845
845
  void mik__inspect_register(JSContext* ctx) {
846
- JSModuleDef* m = JS_NewCModule(ctx, "mikrojs/inspect", mik__inspect_module_init);
846
+ JSModuleDef* m = JS_NewCModule(ctx, "mikro/inspect", mik__inspect_module_init);
847
847
  if (m) {
848
848
  JS_AddModuleExport(ctx, m, "inspect");
849
849
  }
package/src/mik_repl.cpp CHANGED
@@ -263,6 +263,10 @@ static JSValue repl_eval_and_pump(JSContext* ctx, const char* code, size_t len)
263
263
  return pr;
264
264
  }
265
265
  if (state == JS_PROMISE_REJECTED) {
266
+ /* This promise's rejection is about to be reported via the throw
267
+ * path below; drop it from the deferred unhandled-rejection queue
268
+ * so the end-of-turn flush doesn't report it a second time. */
269
+ mik__forget_rejection(ctx, result);
266
270
  JSValue reason = JS_PromiseResult(ctx, result);
267
271
  JS_FreeValue(ctx, result);
268
272
  JS_Throw(ctx, reason);
package/src/mik_sys.cpp CHANGED
@@ -260,7 +260,17 @@ void mik__sys_api_init(JSContext* ctx, JSValue ns) {
260
260
  JS_SetPropertyStr(ctx, ns, "board", mik__sys_board(ctx));
261
261
  JS_SetPropertyStr(ctx, ns, "firmware", mik__sys_firmware(ctx));
262
262
 
263
- const char* device_id = MIK_GetPlatform()->get_device_id();
263
+ /* Guard the platform function POINTERS, not just their results: a
264
+ * platform that omits an optional hook leaves a NULL pointer, and
265
+ * calling it is a segfault with no diagnostics (the sim was down for
266
+ * weeks because platform_node lacked get_reset_reason). */
267
+ const MIKPlatform* platform = MIK_GetPlatform();
268
+ const char* device_id = platform->get_device_id ? platform->get_device_id() : NULL;
264
269
  JS_SetPropertyStr(ctx, ns, "deviceId",
265
270
  device_id ? JS_NewString(ctx, device_id) : JS_UNDEFINED);
271
+
272
+ const char* reset_reason =
273
+ platform->get_reset_reason ? platform->get_reset_reason() : NULL;
274
+ JS_SetPropertyStr(ctx, ns, "resetReason",
275
+ JS_NewString(ctx, reset_reason ? reset_reason : "unknown"));
266
276
  }