@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 +0 -1
- package/cmake/mikrojs_bytecode.cmake +3 -50
- package/include/mikrojs/mikrojs.h +2 -6
- package/include/mikrojs/private.h +12 -9
- package/package.json +3 -3
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/observable/native-observable.node-shim.ts +83 -19
- package/runtime/observable/operators.ts +15 -49
- package/scripts/compile-bytecode.sh +3 -22
- package/scripts/copy-prebuild.js +5 -1
- package/src/builtins.cpp +1 -17
- package/src/mik_abort.cpp +3 -0
- package/src/mik_console.cpp +4 -5
- package/src/mik_observable.cpp +203 -82
- package/src/mik_repl.cpp +3 -3
- package/src/mikrojs.cpp +21 -49
- package/src/timers.cpp +14 -1
- package/src/utils.cpp +11 -2
- package/scripts/extract-atoms.js +0 -144
package/CMakeLists.txt
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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;
|
|
292
|
-
/*
|
|
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.
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
96
|
+
"@mikrojs/registry": "0.18.0"
|
|
97
97
|
},
|
|
98
98
|
"engines": {
|
|
99
99
|
"node": ">=24.0.0"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
9
|
-
//
|
|
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
|
-
/*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
panic(err)
|
|
50
110
|
}
|
|
51
111
|
}
|
|
52
112
|
}
|
|
53
113
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.
|
|
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
|
-
|
|
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.
|
|
131
|
+
if (this._closed) {
|
|
72
132
|
try {
|
|
73
133
|
fn()
|
|
74
134
|
} catch (err) {
|
|
75
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
/*
|
|
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 `
|
|
40
|
+
/* Pass through values matching `predicate`. A throw inside it panics. */
|
|
64
41
|
export const filter =
|
|
65
|
-
<A>(
|
|
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
|
-
|
|
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
|
|
54
|
+
/* Take at most `count` values, then complete. count <= 0 completes immediately. */
|
|
85
55
|
export const take =
|
|
86
|
-
(
|
|
56
|
+
(count: number) =>
|
|
87
57
|
<A>(source: Observable<A>): Observable<A> =>
|
|
88
58
|
new Observable<A>((sub) => {
|
|
89
|
-
if (
|
|
59
|
+
if (count <= 0) {
|
|
90
60
|
sub.complete()
|
|
91
61
|
return
|
|
92
62
|
}
|
|
93
|
-
let remaining =
|
|
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).
|
|
127
|
-
*
|
|
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
|
-
|
|
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>
|
|
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
|
-
|
|
42
|
-
|
|
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"
|
package/scripts/copy-prebuild.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/src/mik_console.cpp
CHANGED
|
@@ -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.
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
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
|