@mikrojs/native 0.6.1 → 0.7.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/mikrojs.h +26 -2
- package/include/mikrojs/private.h +19 -0
- package/package.json +2 -2
- 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/fs/fs.ts +8 -10
- package/runtime/fs/types.ts +3 -3
- package/runtime/http/helpers.ts +44 -32
- package/runtime/http/native.ts +15 -6
- package/runtime/internal.d.ts +2 -2
- package/runtime/kv/shared.ts +10 -8
- package/runtime/kv/types.ts +1 -0
- package/runtime/reader/reader.ts +49 -38
- package/runtime/reader/types.ts +21 -10
- package/runtime/result/result.ts +14 -15
- package/runtime/result/types.ts +4 -14
- package/runtime/schema/schema.ts +5 -4
- package/runtime/stream/stream.ts +73 -45
- package/runtime/stream/types.ts +37 -17
- package/runtime/uart/types.ts +7 -1
- package/runtime/uart/uart.ts +5 -4
- package/src/mik_app_config.cpp +19 -23
- package/src/mik_console.cpp +1 -0
- package/src/mik_repl.cpp +29 -3
- package/src/mikrojs.cpp +46 -17
package/runtime/result/result.ts
CHANGED
|
@@ -7,20 +7,19 @@ export class PanicError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
):
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return constructors as unknown as {
|
|
24
|
-
[K in keyof D & string]: (...args: Parameters<D[K]>) => {name: K} & ReturnType<D[K]>
|
|
10
|
+
export function matchError<E extends {name: string}, R>(
|
|
11
|
+
error: E,
|
|
12
|
+
handlers: {[K in E['name']]: (error: Extract<E, {name: K}>) => R},
|
|
13
|
+
): R {
|
|
14
|
+
// hasOwn guard: bare `handlers[error.name]` would resolve to Object.prototype
|
|
15
|
+
// methods (toString, hasOwnProperty, …) for any error.name string that
|
|
16
|
+
// happens to match a prototype member — silently returning a wrong value
|
|
17
|
+
// instead of failing loudly. The type system enforces `name` is one of the
|
|
18
|
+
// union's tags, but interop with native or hand-built error objects can
|
|
19
|
+
// bypass that.
|
|
20
|
+
const h = handlers as unknown as Record<string, (e: E) => R>
|
|
21
|
+
if (!Object.hasOwn(h, error.name)) {
|
|
22
|
+
throw new TypeError(`matchError: no handler for ${error.name}`)
|
|
25
23
|
}
|
|
24
|
+
return h[error.name]!(error)
|
|
26
25
|
}
|
package/runtime/result/types.ts
CHANGED
|
@@ -37,20 +37,10 @@ export declare function ok<T>(value: T): OkResult<T>
|
|
|
37
37
|
|
|
38
38
|
export declare function err<E>(error: E): ErrResult<E>
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export type ErrorOf<T extends Record<string, (...args: any[]) => any>> = {
|
|
47
|
-
[K in keyof T & string]: ReturnType<T[K]>
|
|
48
|
-
}[keyof T & string]
|
|
49
|
-
|
|
50
|
-
export declare function defineError<D extends VariantDef>(
|
|
51
|
-
name: string,
|
|
52
|
-
variants: D,
|
|
53
|
-
): ErrorConstructors<D>
|
|
40
|
+
export declare function matchError<E extends {name: string}, R>(
|
|
41
|
+
error: E,
|
|
42
|
+
handlers: {[K in E['name']]: (error: Extract<E, {name: K}>) => R},
|
|
43
|
+
): R
|
|
54
44
|
|
|
55
45
|
/** Raw error shape returned by native mik__result_err(). */
|
|
56
46
|
export interface NativeError {
|
package/runtime/schema/schema.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {err, ok} from 'mikrojs/result'
|
|
2
2
|
|
|
3
3
|
import type {Result} from '../result/types.js'
|
|
4
4
|
|
|
5
|
-
export const SchemaError =
|
|
6
|
-
ValidationFailed: (message: string, path: string) =>
|
|
7
|
-
})
|
|
5
|
+
export const SchemaError = {
|
|
6
|
+
ValidationFailed: (message: string, path: string) =>
|
|
7
|
+
({name: 'ValidationFailed', message, path}) as const,
|
|
8
|
+
}
|
|
8
9
|
export type SchemaError = ReturnType<typeof SchemaError.ValidationFailed>
|
|
9
10
|
|
|
10
11
|
// ── Schema types ────────────────────────────────────────────────────
|
package/runtime/stream/stream.ts
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal composable stream primitives for mikrojs.
|
|
3
3
|
*
|
|
4
|
-
* Streams are
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Streams are `AsyncIterable<Result<T, E>>` — Result-yielding rather
|
|
5
|
+
* than throwing — so error handling composes uniformly with the rest
|
|
6
|
+
* of the runtime's Result-based APIs. Combinators short-circuit on the
|
|
7
|
+
* first err item: they yield the err and return.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* shortcuts, tight heap budget.
|
|
9
|
+
* Combinators that originate their own errors (`withTimeout`,
|
|
10
|
+
* `collectUntil`) widen the result's error type with a `StreamError`
|
|
11
|
+
* variant defined in `./types.ts`.
|
|
12
12
|
*
|
|
13
13
|
* V1 scope: enough to rewrite a modem AT channel as a one-file shim.
|
|
14
14
|
* Expand only when a second call site asks for it.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import {err, ok} from 'mikrojs/result'
|
|
18
|
+
|
|
19
|
+
import type {Result} from '../result/types.js'
|
|
20
|
+
|
|
17
21
|
/** Decode a stream of UTF-8 byte chunks into a stream of strings. */
|
|
18
|
-
export async function* decodeUtf8
|
|
22
|
+
export async function* decodeUtf8<E>(
|
|
23
|
+
source: AsyncIterable<Result<Uint8Array, E>>,
|
|
24
|
+
): AsyncIterable<Result<string, E>> {
|
|
19
25
|
const decoder = new TextDecoder()
|
|
20
|
-
for await (const
|
|
21
|
-
|
|
26
|
+
for await (const r of source) {
|
|
27
|
+
if (!r.ok) {
|
|
28
|
+
yield r
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
yield ok(decoder.decode(r.value, {stream: true}))
|
|
32
|
+
}
|
|
33
|
+
const final = decoder.decode()
|
|
34
|
+
if (final.length > 0) yield ok(final)
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
/**
|
|
@@ -36,10 +49,10 @@ export async function* decodeUtf8(source: AsyncIterable<Uint8Array>): AsyncItera
|
|
|
36
49
|
* for protocols that use a blank line as a separator, e.g. HTTP
|
|
37
50
|
* headers vs. body.
|
|
38
51
|
*/
|
|
39
|
-
export async function* splitLines(
|
|
40
|
-
source: AsyncIterable<string
|
|
52
|
+
export async function* splitLines<E>(
|
|
53
|
+
source: AsyncIterable<Result<string, E>>,
|
|
41
54
|
delimiter: string = '\n',
|
|
42
|
-
): AsyncIterable<string
|
|
55
|
+
): AsyncIterable<Result<string, E>> {
|
|
43
56
|
// indexOf('') returns 0 unconditionally, which would make the inner
|
|
44
57
|
// loop spin forever yielding empty strings. Reject at entry instead
|
|
45
58
|
// of hanging the event loop on a user mistake.
|
|
@@ -47,16 +60,20 @@ export async function* splitLines(
|
|
|
47
60
|
throw new RangeError('splitLines: delimiter must be non-empty')
|
|
48
61
|
}
|
|
49
62
|
let buffer = ''
|
|
50
|
-
for await (const
|
|
51
|
-
|
|
63
|
+
for await (const r of source) {
|
|
64
|
+
if (!r.ok) {
|
|
65
|
+
yield r
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
buffer += r.value
|
|
52
69
|
let idx = buffer.indexOf(delimiter)
|
|
53
70
|
while (idx !== -1) {
|
|
54
|
-
yield buffer.slice(0, idx)
|
|
71
|
+
yield ok(buffer.slice(0, idx))
|
|
55
72
|
buffer = buffer.slice(idx + delimiter.length)
|
|
56
73
|
idx = buffer.indexOf(delimiter)
|
|
57
74
|
}
|
|
58
75
|
}
|
|
59
|
-
if (buffer.length > 0) yield buffer
|
|
76
|
+
if (buffer.length > 0) yield ok(buffer)
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
/**
|
|
@@ -68,34 +85,30 @@ export async function* splitLines(
|
|
|
68
85
|
* like modem AT commands where the terminator (`OK`/`ERROR`) is
|
|
69
86
|
* semantically different from the response body lines in between.
|
|
70
87
|
*
|
|
71
|
-
* If the stream closes before any item matches,
|
|
72
|
-
* (
|
|
73
|
-
* own Result type if they want structured error handling.
|
|
88
|
+
* If the stream closes before any item matches, returns
|
|
89
|
+
* `err({name: 'StreamClosed'})`. Source errors propagate unchanged.
|
|
74
90
|
*/
|
|
75
|
-
export async function collectUntil<T>(
|
|
76
|
-
source: AsyncIterable<T
|
|
91
|
+
export async function collectUntil<T, E>(
|
|
92
|
+
source: AsyncIterable<Result<T, E>>,
|
|
77
93
|
predicate: (item: T) => boolean,
|
|
78
|
-
): Promise<{matched: T; collected: T[]}
|
|
94
|
+
): Promise<Result<{matched: T; collected: T[]}, E | {name: 'StreamClosed'}>> {
|
|
79
95
|
const collected: T[] = []
|
|
80
|
-
for await (const
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
collected.push(item)
|
|
96
|
+
for await (const r of source) {
|
|
97
|
+
if (!r.ok) return r
|
|
98
|
+
if (predicate(r.value)) return ok({matched: r.value, collected})
|
|
99
|
+
collected.push(r.value)
|
|
85
100
|
}
|
|
86
|
-
|
|
87
|
-
err.name = 'StreamClosed'
|
|
88
|
-
throw err
|
|
101
|
+
return err({name: 'StreamClosed' as const})
|
|
89
102
|
}
|
|
90
103
|
|
|
91
104
|
/**
|
|
92
105
|
* Wrap an async iterable with a total-duration timeout.
|
|
93
106
|
*
|
|
94
107
|
* The deadline starts when this function is called (not per-item).
|
|
95
|
-
* If the next item doesn't arrive before the deadline,
|
|
96
|
-
*
|
|
97
|
-
* by calling its `return()` method.
|
|
98
|
-
*
|
|
108
|
+
* If the next item doesn't arrive before the deadline, yields
|
|
109
|
+
* `err({name: 'Timeout', ms})` and ends, cleaning up the upstream
|
|
110
|
+
* source by calling its `return()` method. Source errors propagate
|
|
111
|
+
* unchanged.
|
|
99
112
|
*
|
|
100
113
|
* Implementation notes:
|
|
101
114
|
* - Promise.race with setTimeout would leak orphaned timers on the
|
|
@@ -105,29 +118,39 @@ export async function collectUntil<T>(
|
|
|
105
118
|
* - The upstream iterator is explicitly .return()'d on exit (normal,
|
|
106
119
|
* timeout, or consumer break) so underlying resources (UART claims,
|
|
107
120
|
* socket handles, ring buffers) get released.
|
|
121
|
+
*
|
|
122
|
+
* Source contract: the source iterator must surface failures via
|
|
123
|
+
* `yield err(...)` items, not by rejecting `next()`. On a timer win,
|
|
124
|
+
* the in-flight `next()` promise is discarded; if that promise
|
|
125
|
+
* rejects, the rejection is unhandled. Every Result-yielding source
|
|
126
|
+
* in this codebase (`uart.read()`, `fs.readStream`, `Response.body`)
|
|
127
|
+
* satisfies that contract — only relevant if you compose
|
|
128
|
+
* `withTimeout` over a hand-rolled iterator.
|
|
108
129
|
*/
|
|
109
|
-
export async function* withTimeout<T>(
|
|
130
|
+
export async function* withTimeout<T, E>(
|
|
131
|
+
source: AsyncIterable<Result<T, E>>,
|
|
132
|
+
ms: number,
|
|
133
|
+
): AsyncIterable<Result<T, E | {name: 'Timeout'; ms: number}>> {
|
|
110
134
|
const deadline = Date.now() + ms
|
|
111
135
|
const iter = source[Symbol.asyncIterator]()
|
|
112
136
|
try {
|
|
113
137
|
while (true) {
|
|
114
138
|
const remaining = deadline - Date.now()
|
|
115
139
|
if (remaining <= 0) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
throw err
|
|
140
|
+
yield err({name: 'Timeout' as const, ms})
|
|
141
|
+
return
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
let timerId: ReturnType<typeof setTimeout> | undefined
|
|
122
|
-
let result: IteratorResult<T, unknown>
|
|
145
|
+
let result: IteratorResult<Result<T, E>, unknown>
|
|
146
|
+
let timedOut = false
|
|
123
147
|
try {
|
|
124
148
|
result = await Promise.race([
|
|
125
149
|
iter.next(),
|
|
126
|
-
new Promise<IteratorResult<T, unknown>>((
|
|
150
|
+
new Promise<IteratorResult<Result<T, E>, unknown>>((resolve) => {
|
|
127
151
|
timerId = setTimeout(() => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
reject(err)
|
|
152
|
+
timedOut = true
|
|
153
|
+
resolve({done: true, value: undefined})
|
|
131
154
|
}, remaining)
|
|
132
155
|
}),
|
|
133
156
|
])
|
|
@@ -135,8 +158,13 @@ export async function* withTimeout<T>(source: AsyncIterable<T>, ms: number): Asy
|
|
|
135
158
|
if (timerId !== undefined) clearTimeout(timerId)
|
|
136
159
|
}
|
|
137
160
|
|
|
161
|
+
if (timedOut) {
|
|
162
|
+
yield err({name: 'Timeout' as const, ms})
|
|
163
|
+
return
|
|
164
|
+
}
|
|
138
165
|
if (result.done) return
|
|
139
|
-
yield result.value
|
|
166
|
+
yield result.value
|
|
167
|
+
if (!result.value.ok) return
|
|
140
168
|
}
|
|
141
169
|
} finally {
|
|
142
170
|
if (typeof iter.return === 'function') {
|
package/runtime/stream/types.ts
CHANGED
|
@@ -5,42 +5,62 @@
|
|
|
5
5
|
* bytecode at firmware build time. This file is the declaration-only
|
|
6
6
|
* surface consumed by the `mikrojs/stream` re-export in
|
|
7
7
|
* `packages/mikrojs` and by user apps via their tsconfig.
|
|
8
|
+
*
|
|
9
|
+
* Streaming sources yield `Result<T, E>` rather than throwing — see
|
|
10
|
+
* the design note in `stream.ts`. Combinators that originate their own
|
|
11
|
+
* errors widen the error type with a `StreamError` variant.
|
|
8
12
|
*/
|
|
9
13
|
|
|
14
|
+
import type {Result} from 'mikrojs/result'
|
|
15
|
+
|
|
16
|
+
export type StreamError = {name: 'Timeout'; ms: number} | {name: 'StreamClosed'}
|
|
17
|
+
|
|
10
18
|
/** Decode a stream of UTF-8 byte chunks into a stream of strings. */
|
|
11
|
-
export declare function decodeUtf8
|
|
19
|
+
export declare function decodeUtf8<E>(
|
|
20
|
+
source: AsyncIterable<Result<Uint8Array, E>>,
|
|
21
|
+
): AsyncIterable<Result<string, E>>
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
24
|
* Split a stream of text into a stream of lines on the given delimiter
|
|
15
25
|
* (default `\n`). Partial lines spanning chunk boundaries are buffered
|
|
16
26
|
* and reassembled. Empty lines between consecutive delimiters are
|
|
17
|
-
* preserved. Throws `RangeError` if called with an empty delimiter
|
|
27
|
+
* preserved. Throws `RangeError` if called with an empty delimiter
|
|
28
|
+
* (programmer error, not a stream error).
|
|
18
29
|
*/
|
|
19
|
-
export declare function splitLines(
|
|
20
|
-
source: AsyncIterable<string
|
|
30
|
+
export declare function splitLines<E>(
|
|
31
|
+
source: AsyncIterable<Result<string, E>>,
|
|
21
32
|
delimiter?: string,
|
|
22
|
-
): AsyncIterable<string
|
|
33
|
+
): AsyncIterable<Result<string, E>>
|
|
23
34
|
|
|
24
35
|
/**
|
|
25
36
|
* Consume a stream until an item matches `predicate`, then stop.
|
|
26
37
|
*
|
|
27
|
-
* Returns `{matched, collected}` where `collected` is every item
|
|
28
|
-
* came BEFORE the match (not including the matching item).
|
|
29
|
-
*
|
|
30
|
-
* item matches.
|
|
38
|
+
* Returns `ok({matched, collected})` where `collected` is every item
|
|
39
|
+
* that came BEFORE the match (not including the matching item).
|
|
40
|
+
* Returns `err({name: 'StreamClosed'})` if the stream ends before any
|
|
41
|
+
* item matches. Propagates the source's error variants unchanged.
|
|
31
42
|
*/
|
|
32
|
-
export declare function collectUntil<T>(
|
|
33
|
-
source: AsyncIterable<T
|
|
43
|
+
export declare function collectUntil<T, E>(
|
|
44
|
+
source: AsyncIterable<Result<T, E>>,
|
|
34
45
|
predicate: (item: T) => boolean,
|
|
35
|
-
): Promise<{matched: T; collected: T[]}
|
|
46
|
+
): Promise<Result<{matched: T; collected: T[]}, E | Extract<StreamError, {name: 'StreamClosed'}>>>
|
|
36
47
|
|
|
37
48
|
/**
|
|
38
|
-
* Wrap an async iterable with a total-duration deadline.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* timeout, error, or consumer break.
|
|
49
|
+
* Wrap an async iterable with a total-duration deadline. Yields
|
|
50
|
+
* `err({name: 'Timeout', ms})` if the next item doesn't arrive in
|
|
51
|
+
* time, then ends. Releases the upstream iterator via `return()` on
|
|
52
|
+
* timeout, source error, or consumer break.
|
|
53
|
+
*
|
|
54
|
+
* Source contract: the source must surface failures via `yield err(...)`,
|
|
55
|
+
* not by rejecting `next()`. On a timer win the in-flight `next()`
|
|
56
|
+
* promise is discarded — if it later rejects, the rejection is
|
|
57
|
+
* unhandled. All Result-yielding sources in this runtime
|
|
58
|
+
* (`uart.read()`, `fs.readStream`, `Response.body`) satisfy that.
|
|
42
59
|
*/
|
|
43
|
-
export declare function withTimeout<T>(
|
|
60
|
+
export declare function withTimeout<T, E>(
|
|
61
|
+
source: AsyncIterable<Result<T, E>>,
|
|
62
|
+
ms: number,
|
|
63
|
+
): AsyncIterable<Result<T, E | Extract<StreamError, {name: 'Timeout'}>>>
|
|
44
64
|
|
|
45
65
|
/* BufferedReader lives in the sibling `mikrojs/reader` module — see
|
|
46
66
|
* `runtime/reader/` — so apps that only need byte-level framing can
|
package/runtime/uart/types.ts
CHANGED
|
@@ -53,7 +53,13 @@ export interface UartTx {
|
|
|
53
53
|
* @public
|
|
54
54
|
*/
|
|
55
55
|
export interface UartRx {
|
|
56
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Open a Result-yielding async iterable of received chunks. Mid-stream
|
|
58
|
+
* failures (driver fault, port closed mid-iteration) arrive as a single
|
|
59
|
+
* terminal `err(UartError)` item rather than throwing — composes with
|
|
60
|
+
* stream/* combinators and other Result-based APIs.
|
|
61
|
+
*/
|
|
62
|
+
read(): Result<AsyncIterable<Result<Uint8Array, UartError>>, UartError>
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
/**
|
package/runtime/uart/uart.ts
CHANGED
|
@@ -36,14 +36,15 @@ export class Uart implements UartBase {
|
|
|
36
36
|
return this.#native.write(data)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
read(): Result<AsyncIterable<Uint8Array
|
|
39
|
+
read(): Result<AsyncIterable<Result<Uint8Array, UartError>>, UartError> {
|
|
40
40
|
const result = this.#native.read()
|
|
41
41
|
if (!result.ok) return result
|
|
42
42
|
const nativeIter = result.value
|
|
43
|
-
// Wrap the native iterator object (which has next/return) into a proper AsyncIterable
|
|
44
|
-
|
|
43
|
+
// Wrap the native iterator object (which has next/return) into a proper AsyncIterable.
|
|
44
|
+
// Native yields Result items directly via mik__result_ok / mik__result_err_tag.
|
|
45
|
+
const iterable: AsyncIterable<Result<Uint8Array, UartError>> = {
|
|
45
46
|
[Symbol.asyncIterator]() {
|
|
46
|
-
return nativeIter as AsyncIterator<Uint8Array
|
|
47
|
+
return nativeIter as AsyncIterator<Result<Uint8Array, UartError>>
|
|
47
48
|
},
|
|
48
49
|
}
|
|
49
50
|
return ok(iterable)
|
package/src/mik_app_config.cpp
CHANGED
|
@@ -12,30 +12,20 @@
|
|
|
12
12
|
static const char* TAG = "mik_app_config";
|
|
13
13
|
|
|
14
14
|
void MIK_DefaultConfig(MIKConfig* config) {
|
|
15
|
-
config->
|
|
16
|
-
config->restart_delay_ms = 1000;
|
|
15
|
+
config->panic_restart_delay_ms = 1000;
|
|
17
16
|
config->stack_size = 0;
|
|
18
17
|
config->mem_reserved = 64 * 1024;
|
|
19
18
|
config->fs_read_max = 0; /* 0 = runtime default (65536) */
|
|
20
19
|
config->entry_point[0] = '\0';
|
|
21
20
|
config->wifi_country[0] = '\0';
|
|
22
21
|
config->wifi_hostname[0] = '\0';
|
|
22
|
+
config->log_dir[0] = '\0';
|
|
23
|
+
config->log_max_size = 64 * 1024;
|
|
24
|
+
config->log_flush = MIK_LOG_FLUSH_ERROR;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/* Minimal JSON parser for config file — avoids cJSON dependency.
|
|
26
|
-
* Only handles the flat object with
|
|
27
|
-
static bool mik__json_get_bool(const char* json, const char* key, bool* out) {
|
|
28
|
-
char pattern[128];
|
|
29
|
-
snprintf(pattern, sizeof(pattern), "\"%s\"", key);
|
|
30
|
-
const char* p = strstr(json, pattern);
|
|
31
|
-
if (!p) return false;
|
|
32
|
-
p += strlen(pattern);
|
|
33
|
-
while (*p == ' ' || *p == '\t' || *p == ':') p++;
|
|
34
|
-
if (strncmp(p, "true", 4) == 0) { *out = true; return true; }
|
|
35
|
-
if (strncmp(p, "false", 5) == 0) { *out = false; return true; }
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
|
|
28
|
+
* Only handles the flat object with string/number fields we need. */
|
|
39
29
|
static bool mik__json_get_string(const char* json, const char* key, char* out, size_t out_size) {
|
|
40
30
|
char pattern[128];
|
|
41
31
|
snprintf(pattern, sizeof(pattern), "\"%s\"", key);
|
|
@@ -326,14 +316,10 @@ int MIK_LoadConfig(const char* base_path, MIKConfig* config) {
|
|
|
326
316
|
snprintf(path, sizeof(path), "%s/mikro.config.json", app_dir);
|
|
327
317
|
buf = mik__read_json_file(path, &st);
|
|
328
318
|
if (buf) {
|
|
329
|
-
bool bool_val;
|
|
330
319
|
double num_val;
|
|
331
320
|
|
|
332
|
-
if (
|
|
333
|
-
config->
|
|
334
|
-
}
|
|
335
|
-
if (mik__json_get_number(buf, "restartDelay", &num_val)) {
|
|
336
|
-
config->restart_delay_ms = (int)num_val;
|
|
321
|
+
if (mik__json_get_number(buf, "panicRestartDelay", &num_val)) {
|
|
322
|
+
config->panic_restart_delay_ms = (int)num_val;
|
|
337
323
|
}
|
|
338
324
|
if (mik__json_get_number(buf, "stackSize", &num_val)) {
|
|
339
325
|
config->stack_size = (size_t)num_val;
|
|
@@ -349,9 +335,19 @@ int MIK_LoadConfig(const char* base_path, MIKConfig* config) {
|
|
|
349
335
|
mik__json_get_string(buf, "wifi.hostname", config->wifi_hostname,
|
|
350
336
|
sizeof(config->wifi_hostname));
|
|
351
337
|
|
|
338
|
+
mik__json_get_string(buf, "logFile.dir", config->log_dir, sizeof(config->log_dir));
|
|
339
|
+
if (mik__json_get_number(buf, "logFile.maxSize", &num_val)) {
|
|
340
|
+
config->log_max_size = (uint32_t)num_val;
|
|
341
|
+
}
|
|
342
|
+
char flush_str[16];
|
|
343
|
+
if (mik__json_get_string(buf, "logFile.flush", flush_str, sizeof(flush_str))) {
|
|
344
|
+
config->log_flush =
|
|
345
|
+
strcmp(flush_str, "line") == 0 ? MIK_LOG_FLUSH_LINE : MIK_LOG_FLUSH_ERROR;
|
|
346
|
+
}
|
|
347
|
+
|
|
352
348
|
platform->log(MIK_LOG_INFO, TAG,
|
|
353
|
-
"Loaded config:
|
|
354
|
-
config->
|
|
349
|
+
"Loaded config: delay=%dms stack=%u reserved=%lu",
|
|
350
|
+
config->panic_restart_delay_ms,
|
|
355
351
|
(unsigned)config->stack_size, (unsigned long)config->mem_reserved);
|
|
356
352
|
free(buf);
|
|
357
353
|
}
|
package/src/mik_console.cpp
CHANGED
|
@@ -420,6 +420,7 @@ void mik__console_init(JSContext* ctx, JSValue global_obj) {
|
|
|
420
420
|
* bindings they'll never use. The mikrojs/test built-in has a console.log
|
|
421
421
|
* fallback for when these globals aren't present. */
|
|
422
422
|
void MIK_EnableTestHelpers(MIKRuntime* mik_rt) {
|
|
423
|
+
mik_rt->test_mode = true;
|
|
423
424
|
JSContext* ctx = mik_rt->ctx;
|
|
424
425
|
JSValue global_obj = JS_GetGlobalObject(ctx);
|
|
425
426
|
JS_SetPropertyStr(ctx, global_obj, "__testEmit",
|
package/src/mik_repl.cpp
CHANGED
|
@@ -47,6 +47,23 @@ bool mik__repl_is_protocol_mode(void) {
|
|
|
47
47
|
|
|
48
48
|
/* ── TLV frame helpers ───────────────────────────────────────────── */
|
|
49
49
|
|
|
50
|
+
/* Plain bool, not atomic — relies on the assumption that mik__proto_send
|
|
51
|
+
* is only ever called from the REPL serve task, which is the same task
|
|
52
|
+
* that drives mik__console_write (since console_write is invoked by the
|
|
53
|
+
* runtime's stdout/stderr hooks during JS execution, and JS runs on the
|
|
54
|
+
* serve task). If a future feature introduces another task that calls
|
|
55
|
+
* mik__console_write directly (e.g. a watchdog or a sensor task that
|
|
56
|
+
* writes to console), its writes during a proto_send window will be
|
|
57
|
+
* dropped by the file-log tap. Revisit (task-local storage, or move the
|
|
58
|
+
* suppression into mik__console_write itself) if that happens. */
|
|
59
|
+
bool mik__proto_send_in_progress = false;
|
|
60
|
+
|
|
61
|
+
static MIKLogEmitFn s_log_emit_tap = nullptr;
|
|
62
|
+
|
|
63
|
+
void mik__set_log_emit_tap(MIKLogEmitFn fn) {
|
|
64
|
+
s_log_emit_tap = fn;
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
/* Send a TLV-framed message: [ type: uint8 ] [ length: uint32_le ] [ payload ] */
|
|
51
68
|
void mik__proto_send(MIKReplTransport* transport, uint8_t type, const void* data,
|
|
52
69
|
size_t len) {
|
|
@@ -56,10 +73,12 @@ void mik__proto_send(MIKReplTransport* transport, uint8_t type, const void* data
|
|
|
56
73
|
header[2] = (uint8_t)((len >> 8) & 0xFF);
|
|
57
74
|
header[3] = (uint8_t)((len >> 16) & 0xFF);
|
|
58
75
|
header[4] = (uint8_t)((len >> 24) & 0xFF);
|
|
76
|
+
mik__proto_send_in_progress = true;
|
|
59
77
|
transport->write(header, MIK_PROTO_HEADER_SIZE, transport->ctx);
|
|
60
78
|
if (len > 0 && data) {
|
|
61
79
|
transport->write(data, len, transport->ctx);
|
|
62
80
|
}
|
|
81
|
+
mik__proto_send_in_progress = false;
|
|
63
82
|
}
|
|
64
83
|
|
|
65
84
|
void mik__proto_send_ok(MIKReplTransport* transport) {
|
|
@@ -70,9 +89,13 @@ void mik__proto_send_err(MIKReplTransport* transport, const char* msg) {
|
|
|
70
89
|
mik__proto_send(transport, MIK_MSG_ERR, msg, strlen(msg));
|
|
71
90
|
}
|
|
72
91
|
|
|
73
|
-
/* Public wrapper used by mik_console.cpp and mik_stdio.cpp
|
|
92
|
+
/* Public wrapper used by mik_console.cpp and mik_stdio.cpp. Pre-frame
|
|
93
|
+
* tap fires before the TLV bytes go on the wire, so the file logger
|
|
94
|
+
* sees clean body text rather than [type][len][body]. */
|
|
74
95
|
void mik__repl_proto_send_output(uint8_t msg_type, const void* data, size_t len) {
|
|
75
96
|
if (repl_transport) {
|
|
97
|
+
MIKLogEmitFn tap = s_log_emit_tap;
|
|
98
|
+
if (tap) tap(msg_type, data, len);
|
|
76
99
|
mik__proto_send(repl_transport, msg_type, data, len);
|
|
77
100
|
}
|
|
78
101
|
}
|
|
@@ -106,8 +129,11 @@ bool mik__proto_read_exact(MIKReplTransport* transport, void* buf, size_t n) {
|
|
|
106
129
|
}
|
|
107
130
|
/* Microtasks are deferred user JS: settling them re-enters .then
|
|
108
131
|
* handlers that can write to the transport (console.log → MSG_LOG)
|
|
109
|
-
* or touch the filesystem mid-deploy. Gate on pause too
|
|
110
|
-
|
|
132
|
+
* or touch the filesystem mid-deploy. Gate on pause too — and
|
|
133
|
+
* on a pending panic-restart, since the runtime is on its way
|
|
134
|
+
* out and no further user JS should fire on it. */
|
|
135
|
+
if (repl_ctx && !repl_paused &&
|
|
136
|
+
(!repl_mik_rt || repl_mik_rt->restart_at_us == 0)) {
|
|
111
137
|
mik__execute_jobs(repl_ctx);
|
|
112
138
|
}
|
|
113
139
|
/* A pumped job (e.g. the test runtime's __testFileDone) may
|
package/src/mikrojs.cpp
CHANGED
|
@@ -145,18 +145,18 @@ static void mik__promise_rejection_tracker(JSContext* ctx, JSValue promise, JSVa
|
|
|
145
145
|
* are genuine unhandled promise rejections. */
|
|
146
146
|
bool in_promise = !mik__repl_is_evaluating();
|
|
147
147
|
mik__report_uncaught(ctx, reason, in_promise);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
mik_rt->stop_requested = true;
|
|
158
|
-
MIK_Stop(mik_rt);
|
|
148
|
+
/* Notify the error handler (e.g. host bridge) directly.
|
|
149
|
+
* Don't JS_Throw here — we're inside a QuickJS callback
|
|
150
|
+
* during promise resolution; throwing would corrupt engine
|
|
151
|
+
* state and cause mik__execute_jobs to dump the error a
|
|
152
|
+
* second time. MIK_Stop itself gates on whether we're inside
|
|
153
|
+
* an interactive REPL eval so a typo at the prompt doesn't
|
|
154
|
+
* reboot the device. */
|
|
155
|
+
if (mik_rt->error_handler_fn) {
|
|
156
|
+
mik_rt->error_handler_fn(ctx, reason, mik_rt->error_handler_opaque);
|
|
159
157
|
}
|
|
158
|
+
mik_rt->stop_requested = true;
|
|
159
|
+
MIK_Stop(mik_rt);
|
|
160
160
|
return;
|
|
161
161
|
}
|
|
162
162
|
|
|
@@ -534,6 +534,18 @@ void mik__execute_jobs(JSContext* ctx) {
|
|
|
534
534
|
|
|
535
535
|
/* main loop which calls the user JS callbacks */
|
|
536
536
|
int MIK_Loop(MIKRuntime* mik_rt) {
|
|
537
|
+
/* Deferred restart (see MIK_Stop): once the grace window elapses, reboot
|
|
538
|
+
* the device. While we're still in the window, return 0 without pumping
|
|
539
|
+
* timers/consumers so the protocol serve loop keeps reading host
|
|
540
|
+
* commands without firing any more user JS on the dead runtime. The
|
|
541
|
+
* serve loop also skips its microtask drain on this condition. */
|
|
542
|
+
if (mik_rt->restart_at_us > 0) {
|
|
543
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
544
|
+
if (platform->get_boot_us() >= mik_rt->restart_at_us) {
|
|
545
|
+
platform->restart();
|
|
546
|
+
}
|
|
547
|
+
return 0;
|
|
548
|
+
}
|
|
537
549
|
if (mik_rt->stop_requested) {
|
|
538
550
|
return 1;
|
|
539
551
|
}
|
|
@@ -659,14 +671,31 @@ bool MIK_IsStopRequested(MIKRuntime* mik_rt) {
|
|
|
659
671
|
|
|
660
672
|
void MIK_Stop(MIKRuntime* mik_rt) {
|
|
661
673
|
CHECK_NOT_NULL(mik_rt);
|
|
662
|
-
|
|
674
|
+
/* Only firmware (protocol REPL attached) auto-restarts on uncaught
|
|
675
|
+
* exceptions. Host embedders (Node addon, standalone tests) own their
|
|
676
|
+
* own process lifecycle and surface errors via the error handler. */
|
|
677
|
+
if (!MIK_IsReplActive()) {
|
|
663
678
|
return;
|
|
664
679
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
680
|
+
/* A throw inside an interactive REPL eval is a user typo, not an app
|
|
681
|
+
* crash — don't reboot the device. */
|
|
682
|
+
if (mik__repl_is_evaluating()) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
/* In test mode, the supervisor wants stop_requested to bubble up so it
|
|
686
|
+
* can synthesize a failing-test event and move to the next file. Arming
|
|
687
|
+
* a panic-restart here would reboot the device mid-manifest on the
|
|
688
|
+
* first async rejection. */
|
|
689
|
+
if (mik_rt->test_mode) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
/* Defer the restart so the host can still issue deploy/clean/--recover
|
|
693
|
+
* commands during the grace window. The actual platform->restart()
|
|
694
|
+
* fires from MIK_Loop once the deadline elapses. */
|
|
695
|
+
if (mik_rt->restart_at_us == 0) {
|
|
696
|
+
const MIKPlatform* platform = MIK_GetPlatform();
|
|
697
|
+
mik_rt->restart_at_us =
|
|
698
|
+
platform->get_boot_us() + (int64_t)mik_rt->config.panic_restart_delay_ms * 1000;
|
|
670
699
|
}
|
|
671
700
|
}
|
|
672
701
|
|