@mikrojs/native 0.18.0-pr-306.20260731215718 → 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/include/mikrojs/private.h +8 -0
- 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/src/mik_observable.cpp +193 -94
- package/src/mikrojs.cpp +11 -11
- package/src/timers.cpp +14 -1
|
@@ -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;
|
|
@@ -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);
|
|
@@ -259,6 +266,7 @@ JSModuleDef* mik__udp_init(JSContext* ctx);
|
|
|
259
266
|
|
|
260
267
|
/* Observable module (mik_observable.cpp) */
|
|
261
268
|
JSModuleDef* mik__observable_init(JSContext* ctx);
|
|
269
|
+
void mik__observable_dispatch_free(struct MIKRuntime* mik_rt);
|
|
262
270
|
|
|
263
271
|
bool mik__repl_is_evaluating(void);
|
|
264
272
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikrojs/native",
|
|
3
|
-
"version": "0.18.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.18.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.18.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
|
})
|
package/src/mik_observable.cpp
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* mik_observable.cpp — push-
|
|
1
|
+
/* mik_observable.cpp — push-based composable event stream primitive.
|
|
2
2
|
*
|
|
3
3
|
* See .claude/plans/observable.md (worktree branch) for the locked design.
|
|
4
4
|
*
|
|
@@ -9,17 +9,44 @@
|
|
|
9
9
|
* addTeardown, closed
|
|
10
10
|
* - Subscription: unsubscribe
|
|
11
11
|
*
|
|
12
|
-
* Error semantics:
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
12
|
+
* Error semantics: a throw inside an observer, operator, or teardown callback
|
|
13
|
+
* is caught at the dispatch boundary only to report it and apply the app's
|
|
14
|
+
* onPanic policy; it is an application crash like any other uncaught error.
|
|
15
|
+
* Nothing further is delivered: queued entries are dropped, the multicast
|
|
16
|
+
* fan-out stops, from(iterable) stops pulling, and later next/complete calls
|
|
17
|
+
* from the producer's own callback (which JS cannot interrupt mid-function)
|
|
18
|
+
* are no-ops. Teardowns are the exception, since they release resources for
|
|
19
|
+
* work that is already ending; the rest of the chain still runs.
|
|
19
20
|
*
|
|
20
21
|
* Producer-setup throws inside the subscribe callback bubble synchronously
|
|
21
22
|
* to the .subscribe() caller — that's a bug in the producer factory itself,
|
|
22
|
-
* not a runtime dispatch event.
|
|
23
|
+
* not a runtime dispatch event. Teardowns registered before the throw still
|
|
24
|
+
* run: no Subscription reaches the caller, so nothing else could ever
|
|
25
|
+
* release what the producer already acquired.
|
|
26
|
+
*
|
|
27
|
+
* Dispatch trampoline: next/complete invoked while a dispatch is already
|
|
28
|
+
* active (i.e. from inside a handler) enqueue onto a per-runtime FIFO
|
|
29
|
+
* instead of recursing; the outermost dispatch drains the queue before
|
|
30
|
+
* returning. Chain length and re-entrant emission therefore cost O(1)
|
|
31
|
+
* stack per delivery — on-device JS stacks are small and quickjs-ng 0.16
|
|
32
|
+
* frames are large enough that a 3-operator chain used to exhaust a 16 KB
|
|
33
|
+
* limit. Two consequences:
|
|
34
|
+
* - Handler code after a sub.next()/sub.complete() call runs before the
|
|
35
|
+
* downstream handler sees that event (FIFO order is preserved).
|
|
36
|
+
* - complete() closes the subscriber at the call site (complete_pending),
|
|
37
|
+
* so closed/no-op semantics stay synchronous while the complete_fn +
|
|
38
|
+
* teardowns run when the queued entry drains.
|
|
39
|
+
* Delivery is what became O(1) stack, not subscription setup or teardown:
|
|
40
|
+
* subscribe() recurses once per chain layer (user JS calling subscribe), and
|
|
41
|
+
* unsubscribe -> run_teardowns -> upstream.unsubscribe() mirrors it on the
|
|
42
|
+
* way out. Both are paid once per subscription rather than once per value,
|
|
43
|
+
* so neither reintroduces a per-value ceiling.
|
|
44
|
+
* Trade-off: what used to be bounded stack growth is now unbounded heap
|
|
45
|
+
* growth. A synchronous producer emitting N values from inside a dispatch
|
|
46
|
+
* buffers N entries (plus the retained values) instead of recursing N deep.
|
|
47
|
+
* That turns a catchable stack overflow into heap pressure, which on device
|
|
48
|
+
* ends in an OOM panic. Left uncapped deliberately: no shipped producer
|
|
49
|
+
* emits unbounded bursts from inside a handler.
|
|
23
50
|
*/
|
|
24
51
|
|
|
25
52
|
#include <cstddef>
|
|
@@ -33,6 +60,20 @@ extern "C" {
|
|
|
33
60
|
#include "quickjs.h"
|
|
34
61
|
}
|
|
35
62
|
|
|
63
|
+
/* Per-runtime dispatch queue. Entries hold their own references (dup on
|
|
64
|
+
* enqueue, freed after the drained delivery). The queue is only non-empty
|
|
65
|
+
* while a dispatch is on the stack, so runtime teardown never sees values. */
|
|
66
|
+
struct MIKObservableDispatch {
|
|
67
|
+
struct Entry {
|
|
68
|
+
JSValue subscriber;
|
|
69
|
+
JSValue value; /* JS_UNDEFINED for complete entries */
|
|
70
|
+
bool is_complete;
|
|
71
|
+
};
|
|
72
|
+
bool active = false;
|
|
73
|
+
size_t head = 0;
|
|
74
|
+
std::vector<Entry> queue;
|
|
75
|
+
};
|
|
76
|
+
|
|
36
77
|
namespace {
|
|
37
78
|
|
|
38
79
|
JSClassID observable_class_id;
|
|
@@ -46,6 +87,10 @@ struct ObservableData {
|
|
|
46
87
|
struct SubscriberData {
|
|
47
88
|
JSContext* ctx;
|
|
48
89
|
bool closed;
|
|
90
|
+
/* complete() was called while queued dispatch was active: the subscriber
|
|
91
|
+
* is closed to producers, but complete_fn + teardowns run when the queued
|
|
92
|
+
* complete entry drains. */
|
|
93
|
+
bool complete_pending;
|
|
49
94
|
/* observer object retained so its props can't be reclaimed mid-dispatch. */
|
|
50
95
|
JSValue observer;
|
|
51
96
|
JSValue next_fn;
|
|
@@ -61,24 +106,13 @@ struct SubscriptionData {
|
|
|
61
106
|
|
|
62
107
|
/* ── Helpers ─────────────────────────────────────────────────────── */
|
|
63
108
|
|
|
64
|
-
/*
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
(void)magic;
|
|
72
|
-
return JS_Throw(ctx, JS_DupValue(ctx, func_data[0]));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/* Escalation for a panic that cannot be scheduled: report through the
|
|
76
|
-
* allocation-free native uncaught path and halt, mirroring the
|
|
77
|
-
* unhandled-rejection flush. Scheduling fails exactly when the runtime is
|
|
78
|
-
* out of stack or memory — the same conditions that caused the throw — and
|
|
79
|
-
* silence here turned engine-level failures into silently dropped values.
|
|
80
|
-
* Consumes `exception`. */
|
|
81
|
-
static void panic_now(JSContext* ctx, JSValue exception) {
|
|
109
|
+
/* An uncaught throw from a subscriber, operator, or teardown callback is an
|
|
110
|
+
* application crash like any other: report it, notify the host error handler,
|
|
111
|
+
* and apply the app's onPanic policy. Reporting happens here rather than
|
|
112
|
+
* through a deferred re-throw because MIK_Loop stops pumping timers once the
|
|
113
|
+
* panic is armed, so a deferred report would never run. Consumes
|
|
114
|
+
* `exception`. */
|
|
115
|
+
static void panic(JSContext* ctx, JSValue exception) {
|
|
82
116
|
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
83
117
|
if (mik__report_uncaught(ctx, exception, false) && mik_rt) {
|
|
84
118
|
if (mik_rt->error_handler_fn) {
|
|
@@ -89,57 +123,12 @@ static void panic_now(JSContext* ctx, JSValue exception) {
|
|
|
89
123
|
JS_FreeValue(ctx, exception);
|
|
90
124
|
}
|
|
91
125
|
|
|
92
|
-
/*
|
|
93
|
-
* runtime's setTimeout. The synchronous caller keeps going (sibling
|
|
94
|
-
* subscribers receive the value, remaining teardowns run); the eventual
|
|
95
|
-
* uncaught throw halts the runtime via the existing unhandled-rejection
|
|
96
|
-
* path. Stream errors are panics. If the deferred throw cannot be
|
|
97
|
-
* scheduled, escalate via panic_now instead of going silent.
|
|
98
|
-
*
|
|
99
|
-
* Takes ownership of `exception` — caller must not free after this call. */
|
|
100
|
-
static void panic_async(JSContext* ctx, JSValue exception) {
|
|
101
|
-
JSValue global = JS_GetGlobalObject(ctx);
|
|
102
|
-
JSValue setTimeout = JS_GetPropertyStr(ctx, global, "setTimeout");
|
|
103
|
-
JS_FreeValue(ctx, global);
|
|
104
|
-
if (!JS_IsFunction(ctx, setTimeout)) {
|
|
105
|
-
JS_FreeValue(ctx, setTimeout);
|
|
106
|
-
panic_now(ctx, exception);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
JSValueConst data[1] = {exception};
|
|
110
|
-
JSValue thrower = JS_NewCFunctionData(ctx, throw_captured, 0, 0, 1, data);
|
|
111
|
-
if (JS_IsException(thrower)) {
|
|
112
|
-
JSValue stray = JS_GetException(ctx);
|
|
113
|
-
JS_FreeValue(ctx, stray);
|
|
114
|
-
JS_FreeValue(ctx, setTimeout);
|
|
115
|
-
panic_now(ctx, exception);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
JSValue zero = JS_NewInt32(ctx, 0);
|
|
119
|
-
JSValueConst call_args[2] = {thrower, zero};
|
|
120
|
-
JSValue ret = JS_Call(ctx, setTimeout, JS_UNDEFINED, 2, call_args);
|
|
121
|
-
JS_FreeValue(ctx, setTimeout);
|
|
122
|
-
JS_FreeValue(ctx, thrower);
|
|
123
|
-
JS_FreeValue(ctx, zero);
|
|
124
|
-
if (JS_IsException(ret)) {
|
|
125
|
-
/* setTimeout itself threw — under stack exhaustion it fails the
|
|
126
|
-
* same check the dispatch just failed. Escalate. */
|
|
127
|
-
JSValue stray = JS_GetException(ctx);
|
|
128
|
-
JS_FreeValue(ctx, stray);
|
|
129
|
-
panic_now(ctx, exception);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
JS_FreeValue(ctx, ret);
|
|
133
|
-
JS_FreeValue(ctx, exception);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/* Call `fn(argv...)` synchronously; if it throws, schedule the exception
|
|
137
|
-
* to re-throw on the next tick. Caller is not informed of the throw. */
|
|
126
|
+
/* Call `fn(argv...)` synchronously; a throw panics. Caller is not informed. */
|
|
138
127
|
static void run_safely(JSContext* ctx, JSValue fn, int argc, JSValue* argv) {
|
|
139
128
|
JSValue ret = JS_Call(ctx, fn, JS_UNDEFINED, argc, argv);
|
|
140
129
|
if (JS_IsException(ret)) {
|
|
141
130
|
JSValue exc = JS_GetException(ctx);
|
|
142
|
-
|
|
131
|
+
panic(ctx, exc);
|
|
143
132
|
} else {
|
|
144
133
|
JS_FreeValue(ctx, ret);
|
|
145
134
|
}
|
|
@@ -153,15 +142,15 @@ static void invoke_safely(JSContext* ctx, JSValueConst this_val, JSAtom method,
|
|
|
153
142
|
JSValue ret = JS_Invoke(ctx, this_val, method, argc, argv);
|
|
154
143
|
if (JS_IsException(ret)) {
|
|
155
144
|
JSValue exc = JS_GetException(ctx);
|
|
156
|
-
|
|
145
|
+
panic(ctx, exc);
|
|
157
146
|
} else {
|
|
158
147
|
JS_FreeValue(ctx, ret);
|
|
159
148
|
}
|
|
160
149
|
}
|
|
161
150
|
|
|
162
|
-
/* Run all registered teardowns in reverse insertion order.
|
|
163
|
-
*
|
|
164
|
-
*
|
|
151
|
+
/* Run all registered teardowns in reverse insertion order. A throw panics,
|
|
152
|
+
* but the remaining teardowns still run: they release resources for work
|
|
153
|
+
* that is already ending. */
|
|
165
154
|
static void run_teardowns(JSContext* ctx, SubscriberData* d) {
|
|
166
155
|
/* Swap into a local list. If a teardown calls addTeardown synchronously,
|
|
167
156
|
* the SubscriberData.closed flag is already true so addTeardown fires the
|
|
@@ -174,6 +163,66 @@ static void run_teardowns(JSContext* ctx, SubscriberData* d) {
|
|
|
174
163
|
}
|
|
175
164
|
}
|
|
176
165
|
|
|
166
|
+
/* ── Dispatch trampoline ────────────────────────────────────────── */
|
|
167
|
+
|
|
168
|
+
static MIKObservableDispatch* dispatch_state(JSContext* ctx) {
|
|
169
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
170
|
+
return mik_rt ? mik_rt->observable_dispatch : nullptr;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* True once a panic is armed. The producer's own callback keeps running (JS
|
|
174
|
+
* cannot be stopped mid-function), so its later next/complete calls have to
|
|
175
|
+
* become no-ops rather than delivering against crashed state. */
|
|
176
|
+
static bool dispatch_stopped(JSContext* ctx) {
|
|
177
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
178
|
+
return mik_rt && MIK_IsStopRequested(mik_rt);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Deliver a value to the subscriber's next handler. `value` is borrowed. */
|
|
182
|
+
static void deliver_next(JSContext* ctx, SubscriberData* d, JSValue value) {
|
|
183
|
+
if (!JS_IsUndefined(d->next_fn)) {
|
|
184
|
+
run_safely(ctx, d->next_fn, 1, &value);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static void deliver_complete(JSContext* ctx, SubscriberData* d) {
|
|
189
|
+
d->complete_pending = false;
|
|
190
|
+
d->closed = true;
|
|
191
|
+
if (!JS_IsUndefined(d->complete_fn)) {
|
|
192
|
+
run_safely(ctx, d->complete_fn, 0, nullptr);
|
|
193
|
+
}
|
|
194
|
+
run_teardowns(ctx, d);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Drain queued deliveries in FIFO order. Deliveries may enqueue more; the
|
|
198
|
+
* loop keeps going until the queue is empty. Entries whose subscriber closed
|
|
199
|
+
* (unsubscribed) between enqueue and drain are dropped. */
|
|
200
|
+
static void drain(JSContext* ctx, MIKObservableDispatch* ds) {
|
|
201
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
202
|
+
while (ds->head < ds->queue.size()) {
|
|
203
|
+
MIKObservableDispatch::Entry e = ds->queue[ds->head++];
|
|
204
|
+
auto* d = static_cast<SubscriberData*>(JS_GetOpaque(e.subscriber, subscriber_class_id));
|
|
205
|
+
/* A panicked handler means the state these deliveries were queued
|
|
206
|
+
* against may be broken; free the rest without delivering. */
|
|
207
|
+
bool stopped = mik_rt && MIK_IsStopRequested(mik_rt);
|
|
208
|
+
if (d && !d->closed && !stopped) {
|
|
209
|
+
if (e.is_complete) {
|
|
210
|
+
deliver_complete(ctx, d);
|
|
211
|
+
} else {
|
|
212
|
+
deliver_next(ctx, d, e.value);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
JS_FreeValue(ctx, e.subscriber);
|
|
216
|
+
JS_FreeValue(ctx, e.value);
|
|
217
|
+
}
|
|
218
|
+
ds->queue.clear();
|
|
219
|
+
ds->head = 0;
|
|
220
|
+
/* Don't let a one-off burst pin its capacity for the runtime lifetime. */
|
|
221
|
+
if (ds->queue.capacity() > 32) {
|
|
222
|
+
std::vector<MIKObservableDispatch::Entry>().swap(ds->queue);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
177
226
|
/* ── Subscriber ─────────────────────────────────────────────────── */
|
|
178
227
|
|
|
179
228
|
static void subscriber_finalizer(JSRuntime* rt, JSValue val) {
|
|
@@ -206,11 +255,22 @@ static JSClassDef subscriber_class_def = {
|
|
|
206
255
|
static JSValue subscriber_next(JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
|
|
207
256
|
auto* d = static_cast<SubscriberData*>(JS_GetOpaque2(ctx, this_val, subscriber_class_id));
|
|
208
257
|
if (!d) return JS_EXCEPTION;
|
|
209
|
-
if (d->closed) return JS_UNDEFINED;
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
258
|
+
if (d->closed || d->complete_pending) return JS_UNDEFINED;
|
|
259
|
+
if (dispatch_stopped(ctx)) return JS_UNDEFINED;
|
|
260
|
+
JSValue arg = argc > 0 ? argv[0] : JS_UNDEFINED;
|
|
261
|
+
MIKObservableDispatch* ds = dispatch_state(ctx);
|
|
262
|
+
if (!ds) {
|
|
263
|
+
deliver_next(ctx, d, arg);
|
|
264
|
+
return JS_UNDEFINED;
|
|
265
|
+
}
|
|
266
|
+
if (ds->active) {
|
|
267
|
+
ds->queue.push_back({JS_DupValue(ctx, this_val), JS_DupValue(ctx, arg), false});
|
|
268
|
+
return JS_UNDEFINED;
|
|
213
269
|
}
|
|
270
|
+
ds->active = true;
|
|
271
|
+
deliver_next(ctx, d, arg);
|
|
272
|
+
drain(ctx, ds);
|
|
273
|
+
ds->active = false;
|
|
214
274
|
return JS_UNDEFINED;
|
|
215
275
|
}
|
|
216
276
|
|
|
@@ -220,12 +280,22 @@ static JSValue subscriber_complete(JSContext* ctx, JSValueConst this_val, int ar
|
|
|
220
280
|
(void)argv;
|
|
221
281
|
auto* d = static_cast<SubscriberData*>(JS_GetOpaque2(ctx, this_val, subscriber_class_id));
|
|
222
282
|
if (!d) return JS_EXCEPTION;
|
|
223
|
-
if (d->closed) return JS_UNDEFINED;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
283
|
+
if (d->closed || d->complete_pending) return JS_UNDEFINED;
|
|
284
|
+
if (dispatch_stopped(ctx)) return JS_UNDEFINED;
|
|
285
|
+
MIKObservableDispatch* ds = dispatch_state(ctx);
|
|
286
|
+
if (!ds) {
|
|
287
|
+
deliver_complete(ctx, d);
|
|
288
|
+
return JS_UNDEFINED;
|
|
227
289
|
}
|
|
228
|
-
|
|
290
|
+
if (ds->active) {
|
|
291
|
+
d->complete_pending = true;
|
|
292
|
+
ds->queue.push_back({JS_DupValue(ctx, this_val), JS_UNDEFINED, true});
|
|
293
|
+
return JS_UNDEFINED;
|
|
294
|
+
}
|
|
295
|
+
ds->active = true;
|
|
296
|
+
deliver_complete(ctx, d);
|
|
297
|
+
drain(ctx, ds);
|
|
298
|
+
ds->active = false;
|
|
229
299
|
return JS_UNDEFINED;
|
|
230
300
|
}
|
|
231
301
|
|
|
@@ -248,7 +318,7 @@ static JSValue subscriber_add_teardown(JSContext* ctx, JSValueConst this_val, in
|
|
|
248
318
|
static JSValue subscriber_get_closed(JSContext* ctx, JSValueConst this_val) {
|
|
249
319
|
auto* d = static_cast<SubscriberData*>(JS_GetOpaque2(ctx, this_val, subscriber_class_id));
|
|
250
320
|
if (!d) return JS_EXCEPTION;
|
|
251
|
-
return JS_NewBool(ctx, d->closed);
|
|
321
|
+
return JS_NewBool(ctx, d->closed || d->complete_pending);
|
|
252
322
|
}
|
|
253
323
|
|
|
254
324
|
static const JSCFunctionListEntry subscriber_proto_funcs[] = {
|
|
@@ -290,7 +360,10 @@ static JSValue subscription_unsubscribe(JSContext* ctx, JSValueConst this_val, i
|
|
|
290
360
|
if (!sd) return JS_EXCEPTION;
|
|
291
361
|
auto* sub = static_cast<SubscriberData*>(JS_GetOpaque(sd->subscriber_value,
|
|
292
362
|
subscriber_class_id));
|
|
293
|
-
|
|
363
|
+
/* A pending complete owns the close: its queued entry delivers
|
|
364
|
+
* complete_fn + teardowns, matching the recursive-dispatch order where
|
|
365
|
+
* the complete had already run before unsubscribe could. */
|
|
366
|
+
if (!sub || sub->closed || sub->complete_pending) return JS_UNDEFINED;
|
|
294
367
|
sub->closed = true;
|
|
295
368
|
/* unsubscribe() is silent — does NOT call observer.complete().
|
|
296
369
|
* Only natural producer-driven completion fires observer.complete(). */
|
|
@@ -380,6 +453,7 @@ static JSValue subscribe_with_callback(JSContext* ctx, JSValueConst subscribe_cb
|
|
|
380
453
|
auto* d = new SubscriberData{
|
|
381
454
|
ctx,
|
|
382
455
|
false,
|
|
456
|
+
false,
|
|
383
457
|
observer_dup,
|
|
384
458
|
next_fn,
|
|
385
459
|
complete_fn,
|
|
@@ -393,9 +467,20 @@ static JSValue subscribe_with_callback(JSContext* ctx, JSValueConst subscribe_cb
|
|
|
393
467
|
* unsubscribe handles it. */
|
|
394
468
|
JSValue cb_result = JS_Call(ctx, subscribe_cb, JS_UNDEFINED, 1, &subscriber_val);
|
|
395
469
|
if (JS_IsException(cb_result)) {
|
|
396
|
-
/* Producer setup threw — bubble up
|
|
397
|
-
*
|
|
398
|
-
|
|
470
|
+
/* Producer setup threw — bubble up, but release whatever it already
|
|
471
|
+
* acquired first. No Subscription reaches the caller, so a teardown
|
|
472
|
+
* skipped here can never run: the handle it would close is
|
|
473
|
+
* unreachable for the rest of the runtime's life. Park the pending
|
|
474
|
+
* exception while the teardowns run, since they are JS calls and
|
|
475
|
+
* must not inherit it, then restore it for the caller.
|
|
476
|
+
* A queued completion is different: that entry owns the close and
|
|
477
|
+
* runs the teardowns when it drains. */
|
|
478
|
+
if (!d->complete_pending) {
|
|
479
|
+
d->closed = true;
|
|
480
|
+
JSValue pending = JS_GetException(ctx);
|
|
481
|
+
run_teardowns(ctx, d);
|
|
482
|
+
JS_Throw(ctx, pending);
|
|
483
|
+
}
|
|
399
484
|
JS_FreeValue(ctx, subscriber_val);
|
|
400
485
|
return cb_result;
|
|
401
486
|
}
|
|
@@ -562,6 +647,8 @@ static JSValue from_iterable_subscribe(JSContext* ctx, JSValueConst this_val, in
|
|
|
562
647
|
auto* sd = static_cast<SubscriberData*>(
|
|
563
648
|
JS_GetOpaque(subscriber, subscriber_class_id));
|
|
564
649
|
if (!sd || sd->closed) break;
|
|
650
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
651
|
+
if (mik_rt && MIK_IsStopRequested(mik_rt)) break;
|
|
565
652
|
|
|
566
653
|
JSValue value = iterator_next(ctx, iterator, &done);
|
|
567
654
|
if (JS_IsException(value)) {
|
|
@@ -831,9 +918,10 @@ static JSValue multicast_emit_next(JSContext* ctx, JSValueConst this_val, int ar
|
|
|
831
918
|
|
|
832
919
|
JSValue value = argc > 0 ? argv[0] : JS_UNDEFINED;
|
|
833
920
|
JSAtom next_atom = JS_NewAtom(ctx, "next");
|
|
921
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
834
922
|
for (auto& s : snapshot) {
|
|
835
923
|
auto* sd = static_cast<SubscriberData*>(JS_GetOpaque(s, subscriber_class_id));
|
|
836
|
-
if (!sd || sd->closed) {
|
|
924
|
+
if (!sd || sd->closed || (mik_rt && MIK_IsStopRequested(mik_rt))) {
|
|
837
925
|
JS_FreeValue(ctx, s);
|
|
838
926
|
continue;
|
|
839
927
|
}
|
|
@@ -861,9 +949,10 @@ static JSValue multicast_emit_complete(JSContext* ctx, JSValueConst this_val, in
|
|
|
861
949
|
snapshot.swap(m->subscribers);
|
|
862
950
|
|
|
863
951
|
JSAtom complete_atom = JS_NewAtom(ctx, "complete");
|
|
952
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
864
953
|
for (auto& s : snapshot) {
|
|
865
954
|
auto* sd = static_cast<SubscriberData*>(JS_GetOpaque(s, subscriber_class_id));
|
|
866
|
-
if (sd && !sd->closed) {
|
|
955
|
+
if (sd && !sd->closed && !(mik_rt && MIK_IsStopRequested(mik_rt))) {
|
|
867
956
|
invoke_safely(ctx, s, complete_atom, 0, nullptr);
|
|
868
957
|
}
|
|
869
958
|
JS_FreeValue(ctx, s);
|
|
@@ -948,9 +1037,19 @@ static int observable_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
|
948
1037
|
|
|
949
1038
|
} // namespace
|
|
950
1039
|
|
|
1040
|
+
void mik__observable_dispatch_free(MIKRuntime* mik_rt) {
|
|
1041
|
+
delete mik_rt->observable_dispatch;
|
|
1042
|
+
mik_rt->observable_dispatch = nullptr;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
951
1045
|
JSModuleDef* mik__observable_init(JSContext* ctx) {
|
|
952
1046
|
JSRuntime* rt = JS_GetRuntime(ctx);
|
|
953
1047
|
|
|
1048
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
1049
|
+
if (mik_rt && !mik_rt->observable_dispatch) {
|
|
1050
|
+
mik_rt->observable_dispatch = new MIKObservableDispatch();
|
|
1051
|
+
}
|
|
1052
|
+
|
|
954
1053
|
/* Class IDs are runtime-scoped; safe to register once per runtime. */
|
|
955
1054
|
JS_NewClassID(rt, &observable_class_id);
|
|
956
1055
|
JS_NewClass(rt, observable_class_id, &observable_class_def);
|
package/src/mikrojs.cpp
CHANGED
|
@@ -416,6 +416,8 @@ void MIK_FreeRuntime(MIKRuntime* mik_rt) {
|
|
|
416
416
|
delete mik_rt->timers;
|
|
417
417
|
mik_rt->timers = nullptr;
|
|
418
418
|
|
|
419
|
+
mik__observable_dispatch_free(mik_rt);
|
|
420
|
+
|
|
419
421
|
/* Destroy the JS engine. */
|
|
420
422
|
JS_FreeValue(mik_rt->ctx, mik_rt->env_obj);
|
|
421
423
|
mik_rt->env_obj = JS_UNDEFINED;
|
|
@@ -599,6 +601,8 @@ void mik__execute_jobs(JSContext* ctx) {
|
|
|
599
601
|
JS_Throw(ctx1, exc);
|
|
600
602
|
}
|
|
601
603
|
mik_dump_error(ctx1);
|
|
604
|
+
/* Same rule as every other uncaught throw: panic. */
|
|
605
|
+
if (mik_rt) MIK_Stop(mik_rt);
|
|
602
606
|
}
|
|
603
607
|
break;
|
|
604
608
|
}
|
|
@@ -662,6 +666,7 @@ int MIK_Loop(MIKRuntime* mik_rt) {
|
|
|
662
666
|
JS_Throw(mik_rt->ctx, exc);
|
|
663
667
|
}
|
|
664
668
|
mik_dump_error(mik_rt->ctx);
|
|
669
|
+
MIK_Stop(mik_rt);
|
|
665
670
|
return 1;
|
|
666
671
|
}
|
|
667
672
|
mik__stdin_consume(mik_rt->ctx);
|
|
@@ -787,17 +792,12 @@ bool MIK_IsStopRequested(MIKRuntime* mik_rt) {
|
|
|
787
792
|
|
|
788
793
|
void MIK_Stop(MIKRuntime* mik_rt) {
|
|
789
794
|
CHECK_NOT_NULL(mik_rt);
|
|
790
|
-
/*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}
|
|
797
|
-
/* Signal the loop to halt. This is the single place stop_requested is
|
|
798
|
-
* set, so the repl-eval gate above can't be bypassed by a caller. Host
|
|
799
|
-
* embedders (Node addon) observe it via MIK_Loop's return value; the
|
|
800
|
-
* firmware test supervisor reads it via MIK_IsStopRequested. */
|
|
795
|
+
/* REPL-evaluated code gets no exemption: a typo throws synchronously and
|
|
796
|
+
* the eval path reports it without reaching here, so only async fallout
|
|
797
|
+
* from typed code panics. */
|
|
798
|
+
/* The single place stop_requested is set. Host embedders (Node addon)
|
|
799
|
+
* observe it via MIK_Loop's return value; the firmware test supervisor
|
|
800
|
+
* reads it via MIK_IsStopRequested. */
|
|
801
801
|
mik_rt->stop_requested = true;
|
|
802
802
|
/* Only firmware (protocol REPL attached) auto-restarts on uncaught
|
|
803
803
|
* exceptions. Host embedders (Node addon, standalone tests) own their
|
package/src/timers.cpp
CHANGED
|
@@ -165,7 +165,17 @@ void mik__timers_consume(JSContext* ctx) {
|
|
|
165
165
|
if (due_count == 0)
|
|
166
166
|
return;
|
|
167
167
|
|
|
168
|
+
// Timers that ran (or were skipped) this pass; the one-shot cleanup below
|
|
169
|
+
// must not unschedule the ones a panic stopped us from reaching.
|
|
170
|
+
size_t handled = due_count;
|
|
171
|
+
|
|
168
172
|
for (size_t i = 0; i < due_count; i++) {
|
|
173
|
+
// A panic in an earlier callback means the state these were scheduled
|
|
174
|
+
// against may be broken. Leave the rest for the restart.
|
|
175
|
+
if (MIK_IsStopRequested(mik_rt)) {
|
|
176
|
+
handled = i;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
169
179
|
// Re-find the entry — a previous callback may have cleared it
|
|
170
180
|
auto it = std::find_if(timers->entries.begin(), timers->entries.end(),
|
|
171
181
|
[&](const MIKTimerEntry& e) { return e.id == due_ids[i]; });
|
|
@@ -189,6 +199,9 @@ void mik__timers_consume(JSContext* ctx) {
|
|
|
189
199
|
JS_Throw(mik_rt->ctx, exc);
|
|
190
200
|
}
|
|
191
201
|
mik_dump_error(mik_rt->ctx);
|
|
202
|
+
/* Applies the app's onPanic policy; the loop above stops before
|
|
203
|
+
* the next due timer. */
|
|
204
|
+
MIK_Stop(mik_rt);
|
|
192
205
|
}
|
|
193
206
|
JS_FreeValue(mik_rt->ctx, ret);
|
|
194
207
|
JS_FreeValue(mik_rt->ctx, func);
|
|
@@ -198,7 +211,7 @@ void mik__timers_consume(JSContext* ctx) {
|
|
|
198
211
|
}
|
|
199
212
|
|
|
200
213
|
// Remove one-shot timers that are still alive (not already cleared by a callback)
|
|
201
|
-
for (size_t i = 0; i <
|
|
214
|
+
for (size_t i = 0; i < handled; i++) {
|
|
202
215
|
auto it = std::find_if(timers->entries.begin(), timers->entries.end(),
|
|
203
216
|
[&](const MIKTimerEntry& e) { return e.id == due_ids[i]; });
|
|
204
217
|
if (it != timers->entries.end() && !it->is_interval) {
|