@mikrojs/native 0.17.1-pr-300.20260731010039 → 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.
package/CMakeLists.txt CHANGED
@@ -86,7 +86,6 @@ mikrojs_generate_bytecode(
86
86
  MODULE_PREFIX "mikro"
87
87
  SYMBOL_PREFIX "mikro"
88
88
  TARGET gen_bytecode
89
- FREEZE_ATOMS
90
89
  )
91
90
  add_dependencies(mikrojs gen_bytecode)
92
91
  target_include_directories(mikrojs PRIVATE "${gen_bytecode_INCLUDE_DIR}")
@@ -25,11 +25,7 @@ get_filename_component(_MIK_BC_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
25
25
  set(_MIK_BC_SCRIPTS_DIR "${_MIK_BC_CMAKE_DIR}/../scripts" CACHE INTERNAL "")
26
26
 
27
27
  function(mikrojs_generate_bytecode)
28
- # FREEZE_ATOMS: compile blobs against a generated frozen atom table so
29
- # they load zero-copy (JS_READ_OBJ_INPLACE). Core runtime only: the
30
- # device preloads exactly one table, so board/driver packages must
31
- # stay classic (they load via the copy-and-relocate fallback).
32
- cmake_parse_arguments(ARG "FREEZE_ATOMS" "RUNTIME_DIR;MODULE_PREFIX;SYMBOL_PREFIX;TARGET;WORKING_DIRECTORY" "MODULES" ${ARGN})
28
+ cmake_parse_arguments(ARG "" "RUNTIME_DIR;MODULE_PREFIX;SYMBOL_PREFIX;TARGET;WORKING_DIRECTORY" "MODULES" ${ARGN})
33
29
 
34
30
  # Defaults
35
31
  if(NOT ARG_TARGET)
@@ -90,46 +86,7 @@ function(mikrojs_generate_bytecode)
90
86
  WORKING_DIRECTORY ${ARG_WORKING_DIRECTORY}
91
87
  )
92
88
 
93
- # Steps 2a/2b (FREEZE_ATOMS only): pass-1 raw bytecode per module, then
94
- # union the atom sections into the frozen atom table (frozen_atoms.bin
95
- # for qjsc -A, <prefix>_frozen_atoms.h for the runtime preload).
96
- # Module order fixes the table order.
97
- set(_ATOMS_BIN "")
98
- set(_ATOMS_HEADER "")
99
- if(ARG_FREEZE_ATOMS)
100
- set(_EXTRACT_SCRIPT "${_MIK_BC_SCRIPTS_DIR}/extract-atoms.js")
101
- set(_PASS1_BLOBS "")
102
- foreach(mod ${ARG_MODULES})
103
- string(REGEX REPLACE "[^a-zA-Z0-9]" "_" _mod_safe "${mod}")
104
- set(_blob "${_GEN_DIR}/pass1/${_mod_safe}.bjs")
105
- add_custom_command(
106
- OUTPUT ${_blob}
107
- COMMAND ${CMAKE_COMMAND} -E make_directory ${_GEN_DIR}/pass1
108
- COMMAND sh ${_COMPILE_SCRIPT}
109
- ${QJSC_EXECUTABLE}
110
- ${_BUNDLE_DIR}/${mod}.js
111
- ${_blob}
112
- "${ARG_MODULE_PREFIX}/${mod}"
113
- "unused"
114
- DEPENDS ${_BUNDLE_DIR}/bundle.stamp ${_COMPILE_SCRIPT} ${QJSC_EXECUTABLE}
115
- COMMENT "Bytecode pass 1: ${ARG_MODULE_PREFIX}/${mod}"
116
- )
117
- list(APPEND _PASS1_BLOBS ${_blob})
118
- endforeach()
119
-
120
- set(_ATOMS_BIN "${_GEN_DIR}/frozen_atoms.bin")
121
- set(_ATOMS_HEADER "${_GEN_DIR}/${ARG_SYMBOL_PREFIX}_frozen_atoms.h")
122
- add_custom_command(
123
- OUTPUT ${_ATOMS_BIN} ${_ATOMS_HEADER}
124
- COMMAND node ${_EXTRACT_SCRIPT} ${_GEN_DIR} ${ARG_SYMBOL_PREFIX} ${_PASS1_BLOBS}
125
- DEPENDS ${_PASS1_BLOBS} ${_EXTRACT_SCRIPT}
126
- COMMENT "Extracting frozen atom table"
127
- )
128
- endif()
129
-
130
- # Step 2c: Compile each bundled JS file to a bytecode header with qjsc,
131
- # against the frozen atom table so atom references are final ids
132
- # (enables zero-copy loading via JS_READ_OBJ_INPLACE).
89
+ # Step 2: Compile each bundled JS file to a bytecode header with qjsc
133
90
  set(_HEADERS "")
134
91
  foreach(mod ${ARG_MODULES})
135
92
  # Sanitize module name for C identifiers (replace non-alphanumeric with _)
@@ -144,15 +101,11 @@ function(mikrojs_generate_bytecode)
144
101
  ${_header}
145
102
  "${ARG_MODULE_PREFIX}/${mod}"
146
103
  "${ARG_SYMBOL_PREFIX}_${_mod_safe}_bytecode"
147
- ${_ATOMS_BIN}
148
- DEPENDS ${_BUNDLE_DIR}/bundle.stamp ${_COMPILE_SCRIPT} ${QJSC_EXECUTABLE} ${_ATOMS_BIN}
104
+ DEPENDS ${_BUNDLE_DIR}/bundle.stamp ${_COMPILE_SCRIPT} ${QJSC_EXECUTABLE}
149
105
  COMMENT "Compiling bytecode: ${ARG_MODULE_PREFIX}/${mod}"
150
106
  )
151
107
  list(APPEND _HEADERS ${_header})
152
108
  endforeach()
153
- if(_ATOMS_HEADER)
154
- list(APPEND _HEADERS ${_ATOMS_HEADER})
155
- endif()
156
109
 
157
110
  # Step 3: Generate builtins table header (includes + lookup table)
158
111
  set(_TABLE_HEADER "${_GEN_DIR}/${ARG_SYMBOL_PREFIX}_builtins_table.h")
@@ -288,12 +288,8 @@ extern mik_module_desc_t* mik__module_registry_head;
288
288
  * Driver packages use MIK_REGISTER_BUILTIN() to embed their JS wrappers as
289
289
  * firmware builtins, resolved by the real npm package name (e.g. "@mikrojs/your-driver"). */
290
290
  typedef struct mik_ext_builtin_t {
291
- const char* name; /* module name, e.g. "@mikrojs/your-driver" */
292
- /* Compiled bytecode. Must outlive every runtime that imports the
293
- * module: the loader may keep instruction streams pointing into this
294
- * buffer instead of copying them to the heap (JS_READ_OBJ_INPLACE).
295
- * Use a static const array (flash/rodata), never a heap buffer. */
296
- const uint8_t* data;
291
+ const char* name; /* module name, e.g. "@mikrojs/your-driver" */
292
+ const uint8_t* data; /* compiled bytecode */
297
293
  uint32_t data_size;
298
294
  struct mik_ext_builtin_t* next;
299
295
  } mik_ext_builtin_t;
@@ -57,6 +57,9 @@ struct MIKRejectedPromise {
57
57
  JSValue reason;
58
58
  };
59
59
 
60
+ /* Observable dispatch queue (defined in mik_observable.cpp). */
61
+ struct MIKObservableDispatch;
62
+
60
63
  struct MIKRuntime {
61
64
  MIKRunOptions options;
62
65
  MIKConfig config;
@@ -97,10 +100,10 @@ struct MIKRuntime {
97
100
  * here on `reject` and removes it on `handle`; whatever remains after a
98
101
  * microtask drain (mik__execute_jobs) is a genuine unhandled rejection
99
102
  * and gets reported. This deferral mirrors the HTML/WinterCG algorithm
100
- * and is what prevents a transiently-unhandled promise (e.g. a module's
101
- * evaluation promise, rejected before the import loader attaches its
102
- * .then) from being reported. Entries hold dup'd references owned by this
103
- * vector and freed when removed or flushed. */
103
+ * and is what prevents a transiently-unhandled promise (e.g. an async
104
+ * function's result promise, rejected synchronously before `await`
105
+ * attaches its handler) from being reported. Entries hold dup'd
106
+ * references owned by this vector and freed when removed or flushed. */
104
107
  std::vector<MIKRejectedPromise> pending_rejections;
105
108
  /* Shared prototype for Result objects ({ok, value} / {ok, error}) created
106
109
  * by mik__result_ok/mik__result_err and the native:mikro/result ok()/err()
@@ -170,6 +173,10 @@ struct MIKRuntime {
170
173
  * through the wire protocol. NULL means __testEmit is a no-op. */
171
174
  void (*test_emit_fn)(const char* json, size_t len, void* opaque) = nullptr;
172
175
  void* test_emit_opaque = nullptr;
176
+ /* Observable dispatch trampoline state (FIFO + active flag). Allocated by
177
+ * mik__observable_init, freed via mik__observable_dispatch_free. Empty
178
+ * outside an active dispatch, so teardown never sees live JSValues. */
179
+ struct MIKObservableDispatch* observable_dispatch = nullptr;
173
180
  };
174
181
 
175
182
  void mik__pub_fs_register(JSContext* ctx);
@@ -184,12 +191,7 @@ JSValue mik_throw_errno(JSContext* ctx, int err);
184
191
  void mik__execute_jobs(JSContext* ctx);
185
192
  /* End-of-turn unhandled-rejection check; called after each microtask drain. */
186
193
  void mik__flush_unhandled_rejections(JSContext* ctx);
187
- /* Drop a promise from the pending-rejection queue without reporting it. */
188
- void mik__forget_rejection(JSContext* ctx, JSValue promise);
189
194
  JSModuleDef* mik__load_builtin(JSContext* ctx, const char* name);
190
- /* Preload the generated frozen atom table (must run right after
191
- * JS_NewRuntime2, before any context/intrinsic creates atoms). */
192
- int mik__preload_frozen_atoms(JSRuntime* rt);
193
195
  int mik__load_file(JSContext* ctx, DynBuf* dbuf, const char* filename);
194
196
  void mik__resolve_fs_path(JSContext* ctx, const char* module_name, char* out, size_t out_size);
195
197
  int mik__resolve_fs_root(JSContext* ctx, const char* path, char* out, size_t out_size);
@@ -264,6 +266,7 @@ JSModuleDef* mik__udp_init(JSContext* ctx);
264
266
 
265
267
  /* Observable module (mik_observable.cpp) */
266
268
  JSModuleDef* mik__observable_init(JSContext* ctx);
269
+ void mik__observable_dispatch_free(struct MIKRuntime* mik_rt);
267
270
 
268
271
  bool mik__repl_is_evaluating(void);
269
272
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikrojs/native",
3
- "version": "0.17.1-pr-300.20260731010039",
3
+ "version": "0.18.0",
4
4
  "description": "Mikro.js C++ runtime library and Node.js native addon",
5
5
  "keywords": [
6
6
  "esp32",
@@ -86,14 +86,14 @@
86
86
  "cmake-js": "^8.0.0",
87
87
  "node-addon-api": "^8.7.0",
88
88
  "node-gyp-build": "^4.8.4",
89
- "@mikrojs/quickjs": "0.17.1-pr-300.20260731010039+a6350ad"
89
+ "@mikrojs/quickjs": "0.18.0"
90
90
  },
91
91
  "devDependencies": {
92
92
  "@swc/core": "^1.15.30",
93
93
  "@types/node": "^24.12.2",
94
94
  "esbuild": "^0.28.0",
95
95
  "terser": "^5.46.2",
96
- "@mikrojs/registry": "0.17.1-pr-300.20260731010039+a6350ad"
96
+ "@mikrojs/registry": "0.18.0"
97
97
  },
98
98
  "engines": {
99
99
  "node": ">=24.0.0"
@@ -5,24 +5,48 @@
5
5
  // - subscribe() returns a Subscription with unsubscribe() (no AbortSignal)
6
6
  // - no error channel — throws inside dispatch or teardown are caught at the
7
7
  // boundary, isolated to the offending subscriber, and re-thrown
8
- // asynchronously via setTimeout(0) so the synchronous producer keeps
9
- // running but the bug eventually surfaces (and on device, halts the
10
- // runtime via the existing unhandled-rejection path)
8
+ // asynchronously via setTimeout(0) so the synchronous producer finishes
9
+ // the dispatch; on device the deferred throw then panics
11
10
  // - sync emission allowed
12
11
  // - pipe-only composition (operators live in operators.ts)
13
12
  // - withEmitters() factory: {observable, next, complete}
14
13
 
15
- /* Catch a thrown error and re-throw it on the next tick. The synchronous
16
- * caller keeps going (other subscribers receive the value, remaining
17
- * teardowns run); the error eventually surfaces as an uncaught exception. */
18
- function panicAsync(err: unknown): void {
14
+ /* A throw in a subscriber, operator, or teardown callback is an application
15
+ * crash. On device it stops the runtime per onPanic, so nothing further is
16
+ * delivered. There is no runtime to stop here, so the host sees an uncaught
17
+ * error on the next tick and `panicked` suppresses further delivery, which is
18
+ * the part a unit test can observe. It clears on the next fresh subscribe, so
19
+ * one test's crash cannot poison the next; on device the runtime is gone. */
20
+ let panicked = false
21
+
22
+ function panic(err: unknown): void {
23
+ panicked = true
19
24
  setTimeout(() => {
20
25
  throw err
21
26
  }, 0)
22
27
  }
23
28
 
29
+ /* Dispatch trampoline, mirroring mik_observable.cpp: next/complete called
30
+ * while a dispatch is active enqueue instead of recursing; the outermost
31
+ * dispatch drains in FIFO order. Handler code after sub.next() therefore
32
+ * runs before downstream delivery. */
33
+ type QueueEntry = {sub: Subscriber<unknown>; value: unknown; isComplete: boolean}
34
+ const queue: QueueEntry[] = []
35
+ let queueHead = 0
36
+ let dispatchActive = false
37
+
38
+ function drainQueue(): void {
39
+ while (queueHead < queue.length) {
40
+ const e = queue[queueHead++]!
41
+ if (!panicked) e.sub.deliverQueued(e)
42
+ }
43
+ queue.length = 0
44
+ queueHead = 0
45
+ }
46
+
24
47
  class Subscriber<T> {
25
- closed = false
48
+ private _closed = false
49
+ private completePending = false
26
50
  private next_fn: ((v: T) => void) | undefined
27
51
  private complete_fn: (() => void) | undefined
28
52
  private teardowns: Array<() => void> = []
@@ -40,25 +64,61 @@ class Subscriber<T> {
40
64
  }
41
65
  }
42
66
 
67
+ get closed(): boolean {
68
+ return this._closed || this.completePending
69
+ }
70
+
43
71
  next(value: T): void {
44
- if (this.closed) return
72
+ if (this.closed || panicked) return
73
+ if (dispatchActive) {
74
+ queue.push({sub: this as Subscriber<unknown>, value, isComplete: false})
75
+ return
76
+ }
77
+ dispatchActive = true
78
+ this.deliverNext(value)
79
+ drainQueue()
80
+ dispatchActive = false
81
+ }
82
+
83
+ complete(): void {
84
+ if (this.closed || panicked) return
85
+ if (dispatchActive) {
86
+ this.completePending = true
87
+ queue.push({sub: this as Subscriber<unknown>, value: undefined, isComplete: true})
88
+ return
89
+ }
90
+ dispatchActive = true
91
+ this.deliverComplete()
92
+ drainQueue()
93
+ dispatchActive = false
94
+ }
95
+
96
+ /* Deliver a queued entry; entries whose subscriber closed (unsubscribed)
97
+ * between enqueue and drain are dropped. */
98
+ deliverQueued(e: QueueEntry): void {
99
+ if (this._closed) return
100
+ if (e.isComplete) this.deliverComplete()
101
+ else this.deliverNext(e.value as T)
102
+ }
103
+
104
+ private deliverNext(value: T): void {
45
105
  if (this.next_fn) {
46
106
  try {
47
107
  this.next_fn(value)
48
108
  } catch (err) {
49
- panicAsync(err)
109
+ panic(err)
50
110
  }
51
111
  }
52
112
  }
53
113
 
54
- complete(): void {
55
- if (this.closed) return
56
- this.closed = true
114
+ private deliverComplete(): void {
115
+ this.completePending = false
116
+ this._closed = true
57
117
  if (this.complete_fn) {
58
118
  try {
59
119
  this.complete_fn()
60
120
  } catch (err) {
61
- panicAsync(err)
121
+ panic(err)
62
122
  }
63
123
  }
64
124
  this.runTeardowns()
@@ -68,11 +128,11 @@ class Subscriber<T> {
68
128
  if (typeof fn !== 'function') {
69
129
  throw new TypeError('addTeardown: argument must be a function')
70
130
  }
71
- if (this.closed) {
131
+ if (this._closed) {
72
132
  try {
73
133
  fn()
74
134
  } catch (err) {
75
- panicAsync(err)
135
+ panic(err)
76
136
  }
77
137
  return
78
138
  }
@@ -80,9 +140,10 @@ class Subscriber<T> {
80
140
  }
81
141
 
82
142
  // Used by Subscription.unsubscribe — silent (no observer.complete call).
143
+ // A pending complete owns the close; its queued entry still delivers.
83
144
  closeSilently(): void {
84
145
  if (this.closed) return
85
- this.closed = true
146
+ this._closed = true
86
147
  this.runTeardowns()
87
148
  }
88
149
 
@@ -93,7 +154,7 @@ class Subscriber<T> {
93
154
  try {
94
155
  list[i]!()
95
156
  } catch (err) {
96
- panicAsync(err)
157
+ panic(err)
97
158
  }
98
159
  }
99
160
  }
@@ -122,6 +183,7 @@ export class Observable<Ok, Err = never> {
122
183
  }
123
184
 
124
185
  subscribe(observer?: unknown): Subscription {
186
+ if (!dispatchActive) panicked = false
125
187
  const sub = new Subscriber<unknown>(observer)
126
188
  try {
127
189
  this.#cb(sub)
@@ -167,7 +229,7 @@ export class Observable<Ok, Err = never> {
167
229
  ) {
168
230
  return new Observable<unknown>((sub) => {
169
231
  for (const value of src as Iterable<unknown>) {
170
- if (sub.closed) return
232
+ if (sub.closed || panicked) return
171
233
  sub.next(value)
172
234
  }
173
235
  if (!sub.closed) sub.complete()
@@ -201,6 +263,7 @@ export class Observable<Ok, Err = never> {
201
263
  // Snapshot to be resilient against mid-dispatch unsubscribes.
202
264
  const snapshot = subs.slice()
203
265
  for (const s of snapshot) {
266
+ if (panicked) break
204
267
  if (!s.closed) s.next(value)
205
268
  }
206
269
  }
@@ -211,6 +274,7 @@ export class Observable<Ok, Err = never> {
211
274
  const snapshot = subs.slice()
212
275
  subs.length = 0
213
276
  for (const s of snapshot) {
277
+ if (panicked) break
214
278
  if (!s.closed) s.complete()
215
279
  }
216
280
  }
@@ -7,12 +7,9 @@
7
7
  * Result-aware operators (`mapOk`, `filterOk`, ...) ship when a concrete
8
8
  * consumer asks. Today no module produces fallible event streams.
9
9
  *
10
- * Errors: throws inside operator transforms or finalize callbacks are caught
11
- * at the dispatch boundary, isolated to that subscriber, and re-thrown
12
- * asynchronously via setTimeout(0). The synchronous producer keeps going
13
- * (the bad value is dropped, sibling subscribers untouched), and on device
14
- * the eventual uncaught throw halts the runtime via the existing
15
- * unhandled-rejection path. Stream errors are panics.
10
+ * Errors: a throw inside a transform or a finalize callback propagates to the
11
+ * dispatch boundary, which reports it and panics. Operators do not catch:
12
+ * an application crash is an application crash.
16
13
  *
17
14
  * See `.claude/plans/observable.md` for the full design.
18
15
  */
@@ -28,69 +25,42 @@ import type {Observable as ObservableT} from './types.js'
28
25
  const Observable = NativeObservable as unknown as typeof ObservableT
29
26
  type Observable<Ok, Err = never> = ObservableT<Ok, Err>
30
27
 
31
- /* Catch a thrown error and re-throw it on the next tick. The synchronous
32
- * caller keeps going; the error eventually surfaces as an uncaught
33
- * exception. */
34
- function panicAsync(err: unknown): void {
35
- setTimeout(() => {
36
- throw err
37
- }, 0)
38
- }
39
-
40
- /* Map values through a transform. Throws inside `fn` are caught and
41
- * scheduled to re-throw on the next tick (panic). The bad value is dropped
42
- * for that subscription; sibling subscriptions are unaffected. */
28
+ /* Map values through a transform. A throw inside `fn` panics. */
43
29
  export const map =
44
30
  <A, B>(fn: (value: A) => B) =>
45
31
  (source: Observable<A>): Observable<B> =>
46
32
  new Observable<B>((sub) => {
47
33
  const upstream = source.subscribe({
48
- next: (value) => {
49
- let next: B
50
- try {
51
- next = fn(value)
52
- } catch (err) {
53
- panicAsync(err)
54
- return
55
- }
56
- sub.next(next)
57
- },
34
+ next: (value) => sub.next(fn(value)),
58
35
  complete: () => sub.complete(),
59
36
  })
60
37
  sub.addTeardown(() => upstream.unsubscribe())
61
38
  })
62
39
 
63
- /* Pass through values matching `pred`. `pred` errors panic asynchronously. */
40
+ /* Pass through values matching `predicate`. A throw inside it panics. */
64
41
  export const filter =
65
- <A>(pred: (value: A) => boolean) =>
42
+ <A>(predicate: (value: A) => boolean) =>
66
43
  (source: Observable<A>): Observable<A> =>
67
44
  new Observable<A>((sub) => {
68
45
  const upstream = source.subscribe({
69
46
  next: (value) => {
70
- let keep: boolean
71
- try {
72
- keep = pred(value)
73
- } catch (err) {
74
- panicAsync(err)
75
- return
76
- }
77
- if (keep) sub.next(value)
47
+ if (predicate(value)) sub.next(value)
78
48
  },
79
49
  complete: () => sub.complete(),
80
50
  })
81
51
  sub.addTeardown(() => upstream.unsubscribe())
82
52
  })
83
53
 
84
- /* Take at most n values, then complete. n <= 0 completes immediately. */
54
+ /* Take at most `count` values, then complete. count <= 0 completes immediately. */
85
55
  export const take =
86
- (n: number) =>
56
+ (count: number) =>
87
57
  <A>(source: Observable<A>): Observable<A> =>
88
58
  new Observable<A>((sub) => {
89
- if (n <= 0) {
59
+ if (count <= 0) {
90
60
  sub.complete()
91
61
  return
92
62
  }
93
- let remaining = n
63
+ let remaining = count
94
64
  const upstream = source.subscribe({
95
65
  next: (value) => {
96
66
  if (remaining <= 0) return
@@ -123,8 +93,8 @@ export const takeUntil =
123
93
  })
124
94
 
125
95
  /* Run `fn` when the subscription ends for any reason (unsubscribe or
126
- * natural completion). Throws inside `fn` are caught and panic
127
- * asynchronously, so subsequent teardowns still run. RxJS naming. */
96
+ * natural completion). A throw inside `fn` panics; the remaining teardowns
97
+ * still run. RxJS naming. */
128
98
  export const finalize =
129
99
  (fn: () => void) =>
130
100
  <A>(source: Observable<A>): Observable<A> =>
@@ -135,10 +105,6 @@ export const finalize =
135
105
  })
136
106
  sub.addTeardown(() => {
137
107
  upstream.unsubscribe()
138
- try {
139
- fn()
140
- } catch (err) {
141
- panicAsync(err)
142
- }
108
+ fn()
143
109
  })
144
110
  })
@@ -1,12 +1,9 @@
1
1
  #!/bin/sh
2
2
  # Compile a bundled JS module to a C bytecode header using qjsc.
3
3
  #
4
- # Usage: compile-bytecode.sh <qjsc> <input.js> <output.h> <module_name> <symbol_name> [atoms.bin]
4
+ # Usage: compile-bytecode.sh <qjsc> <input.js> <output.h> <module_name> <symbol_name>
5
5
  #
6
6
  # Reads <input>.externals for external module declarations.
7
- # If <output> ends in .bjs, emits raw bytecode instead of a C header
8
- # (pass 1 of the frozen-atom pipeline; see extract-atoms.js).
9
- # If [atoms.bin] is given, compiles against that frozen atom table (-A).
10
7
 
11
8
  set -e
12
9
 
@@ -15,7 +12,6 @@ INPUT="$2"
15
12
  OUTPUT="$3"
16
13
  MODULE_NAME="$4"
17
14
  SYMBOL_NAME="$5"
18
- ATOMS_BIN="$6"
19
15
 
20
16
  EXTERNALS_FILE="${INPUT%.js}.externals"
21
17
 
@@ -38,20 +34,5 @@ fi
38
34
  # so any runtime module that reads `import.meta.*` (e.g. mikrojs/env
39
35
  # accessing import.meta.env) breaks at runtime. The ~1 KB heap saving
40
36
  # from stripping debug isn't worth crippling a spec-standard module API.
41
- A_FLAGS=""
42
- if [ -n "$ATOMS_BIN" ]; then
43
- A_FLAGS="-A $ATOMS_BIN"
44
- fi
45
-
46
- case "$OUTPUT" in
47
- *.bjs)
48
- # Raw bytecode (frozen-atom pass 1). Must use the same flags as the
49
- # header pass so the atom set matches, minus -N (no C symbol).
50
- # shellcheck disable=SC2086
51
- "$QJSC" -b -m -s -n "$MODULE_NAME" $M_FLAGS $A_FLAGS -o "$OUTPUT" "$INPUT"
52
- ;;
53
- *)
54
- # shellcheck disable=SC2086
55
- "$QJSC" -m -s -N "$SYMBOL_NAME" -n "$MODULE_NAME" $M_FLAGS $A_FLAGS -o "$OUTPUT" "$INPUT"
56
- ;;
57
- esac
37
+ # shellcheck disable=SC2086
38
+ "$QJSC" -m -s -N "$SYMBOL_NAME" -n "$MODULE_NAME" $M_FLAGS -o "$OUTPUT" "$INPUT"
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import {copyFileSync, existsSync, mkdirSync} from 'node:fs'
2
+ import {copyFileSync, existsSync, mkdirSync, rmSync} from 'node:fs'
3
3
  import {dirname, resolve} from 'node:path'
4
4
  import {fileURLToPath} from 'node:url'
5
5
 
@@ -15,6 +15,10 @@ if (!existsSync(source)) {
15
15
  const targetDir = resolve(pkgRoot, 'prebuilds', `${process.platform}-${process.arch}`)
16
16
  mkdirSync(targetDir, {recursive: true})
17
17
  const target = resolve(targetDir, 'mikrojs.napi.node')
18
+ // Remove before copying so the target gets a fresh inode. Overwriting in
19
+ // place invalidates the macOS code-signature cache and any process that
20
+ // dlopens the library afterwards is killed with SIGKILL.
21
+ rmSync(target, {force: true})
18
22
  copyFileSync(source, target)
19
23
  // eslint-disable-next-line no-console
20
24
  console.log(`Copied → ${target}`)
package/src/builtins.cpp CHANGED
@@ -17,19 +17,6 @@ typedef struct {
17
17
  #include "gen/mikro_builtins_table.h"
18
18
  #define builtins mikro_builtins
19
19
 
20
- /* Frozen atom table generated alongside the builtin bytecode: preloading it
21
- * right after runtime creation gives builtin blobs final atom ids, letting
22
- * their instruction streams load zero-copy from flash/rodata. */
23
- #include "gen/mikro_frozen_atoms.h"
24
-
25
- int mik__preload_frozen_atoms(JSRuntime* rt) {
26
- if (mikro_frozen_atom_count == 0) {
27
- return 0;
28
- }
29
- return JS_PreloadFrozenAtoms(rt, mikro_frozen_atom_names, mikro_frozen_atom_lens,
30
- mikro_frozen_atom_count);
31
- }
32
-
33
20
  /* External builtins registered by board/driver packages via MIK_REGISTER_BUILTIN() */
34
21
  // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
35
22
  mik_ext_builtin_t* mik__ext_builtin_head = nullptr;
@@ -42,10 +29,7 @@ static JSModuleDef* mik__deserialize_builtin(JSContext* ctx, const char* name,
42
29
  platform->log(MIK_LOG_DEBUG, "mikrojs", "Loading builtin '%s' (%u bytes, bc_version=%u, data_ptr=%p)", name,
43
30
  data_size, data_size > 0 ? data[0] : 0, (void*)data);
44
31
 
45
- /* Builtin bytecode lives in flash/rodata for the runtime's lifetime, so
46
- * qualifying instruction streams stay in place instead of being copied
47
- * to the JS heap (falls back per function for non-final atom refs). */
48
- JSValue obj = JS_ReadObject(ctx, data, data_size, JS_READ_OBJ_BYTECODE | JS_READ_OBJ_INPLACE);
32
+ JSValue obj = JS_ReadObject(ctx, data, data_size, JS_READ_OBJ_BYTECODE);
49
33
 
50
34
  if (JS_IsException(obj)) {
51
35
  /* Peek the exception for the log, then re-throw it so the caller
package/src/mik_abort.cpp CHANGED
@@ -65,6 +65,9 @@ static JSValue mik__abort_lazy_get(JSContext* ctx, JSValue this_val, int magic)
65
65
  }
66
66
  if (JS_PromiseState(ctx, ret) == JS_PROMISE_REJECTED) {
67
67
  JSValue err = JS_PromiseResult(ctx, ret);
68
+ /* The rejection is re-thrown below; mark the promise handled so the
69
+ * unhandled-rejection flush doesn't report the same error again. */
70
+ JS_PromiseMarkAsHandled(ctx, ret);
68
71
  JS_FreeValue(ctx, ret);
69
72
  JS_FreeValue(ctx, global_obj);
70
73
  return JS_Throw(ctx, err);
@@ -237,11 +237,10 @@ static JSValue mik__console_error_warn(JSContext* ctx, JSValue this_val, int arg
237
237
  /* ── Uncaught error reporting ─────────────────────────────────────── */
238
238
 
239
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
240
+ * once for a single failure. The sync throw path (mik_dump_error) can report
241
+ * an error whose promise still sits in the deferred flush queue, e.g. an
242
+ * entry module whose rejected eval promise is read by value while the host
243
+ * dumps the pending exception. Mark the error object the first
245
244
  * time it is reported and skip it afterwards. Keying on object identity is
246
245
  * exact and time-independent; a recycled address belongs to a fresh object
247
246
  * with no marker, so distinct errors are never suppressed. Primitive