@mikrojs/native 0.6.0 → 0.8.0-pr-85.g6768f8c
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 +5 -16
- package/runtime/result/types.ts +4 -14
- package/runtime/schema/schema.ts +5 -4
- package/runtime/stream/stream.ts +65 -45
- package/runtime/stream/types.ts +31 -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 +36 -5
- package/src/mikrojs.cpp +46 -17
|
@@ -23,15 +23,25 @@ typedef struct MIKRunOptions {
|
|
|
23
23
|
bool use_psram_heap;
|
|
24
24
|
} MIKRunOptions;
|
|
25
25
|
|
|
26
|
+
/* File-log flush policies. Mirror of `LogFlush` in the host-side TS type. */
|
|
27
|
+
typedef enum MIKLogFlush {
|
|
28
|
+
MIK_LOG_FLUSH_ERROR = 0, /* default: buffer, flush on warn/error or buffer full */
|
|
29
|
+
MIK_LOG_FLUSH_LINE = 1,
|
|
30
|
+
} MIKLogFlush;
|
|
31
|
+
|
|
26
32
|
typedef struct MIKConfig {
|
|
27
|
-
|
|
28
|
-
int restart_delay_ms;
|
|
33
|
+
int panic_restart_delay_ms;
|
|
29
34
|
size_t stack_size;
|
|
30
35
|
uint32_t mem_reserved;
|
|
31
36
|
uint32_t fs_read_max; /* 0 = keep runtime default (65536) */
|
|
32
37
|
char entry_point[128];
|
|
33
38
|
char wifi_country[3]; /* Two-letter country code + NUL, e.g. "NO" */
|
|
34
39
|
char wifi_hostname[64]; /* DHCP hostname; empty = use mikrojs-<device-id> default */
|
|
40
|
+
/* File-log config. log_dir empty disables file logging; otherwise the
|
|
41
|
+
* file lives at "<log_dir>/log.txt" (rotated as ".../log.txt.1"). */
|
|
42
|
+
char log_dir[64];
|
|
43
|
+
uint32_t log_max_size;
|
|
44
|
+
MIKLogFlush log_flush;
|
|
35
45
|
} MIKConfig;
|
|
36
46
|
|
|
37
47
|
void MIK_DefaultConfig(MIKConfig* config);
|
|
@@ -306,6 +316,20 @@ void mik__proto_send(MIKReplTransport* transport, uint8_t type, const void* data
|
|
|
306
316
|
void mik__proto_send_ok(MIKReplTransport* transport);
|
|
307
317
|
void mik__proto_send_err(MIKReplTransport* transport, const char* msg);
|
|
308
318
|
|
|
319
|
+
/* Tap called by mik__repl_proto_send_output with the raw body bytes (no
|
|
320
|
+
* TLV framing) before they're sent on the wire. Single slot — owned by
|
|
321
|
+
* the file logger on the device side. msg_type is the MIK_MSG_* value so
|
|
322
|
+
* the tap can pick the appropriate log level. */
|
|
323
|
+
typedef void (*MIKLogEmitFn)(uint8_t msg_type, const void* data, size_t len);
|
|
324
|
+
void mik__set_log_emit_tap(MIKLogEmitFn fn);
|
|
325
|
+
|
|
326
|
+
/* True while mik__proto_send is writing TLV bytes to the transport.
|
|
327
|
+
* Platform-side transports (mik__console_write on the ESP32) check this
|
|
328
|
+
* to skip any installed wire-byte taps that would otherwise capture the
|
|
329
|
+
* framing in addition to the body. Single-task code path, so plain bool
|
|
330
|
+
* is sufficient — there's no other writer of this flag. */
|
|
331
|
+
extern bool mik__proto_send_in_progress;
|
|
332
|
+
|
|
309
333
|
/* ── Session primitives ─────────────────────────────────────────────
|
|
310
334
|
* A protocol session is scoped to a transport (one client connection).
|
|
311
335
|
* Runtimes are attached/detached within the session; a supervisor can
|
|
@@ -56,6 +56,18 @@ struct MIKRuntime {
|
|
|
56
56
|
bool is_worker;
|
|
57
57
|
bool freeing;
|
|
58
58
|
bool stop_requested; /* Set by promise rejection tracker to stop the loop */
|
|
59
|
+
/* Set by MIK_EnableTestHelpers — the test supervisor wants stop_requested
|
|
60
|
+
* to bubble up cleanly so it can synthesize a failing-test event and move
|
|
61
|
+
* to the next file. Without this flag, MIK_Stop would arm a panic-restart
|
|
62
|
+
* deadline and reboot the whole device mid-manifest on the first async
|
|
63
|
+
* rejection. */
|
|
64
|
+
bool test_mode;
|
|
65
|
+
/* Deferred restart deadline (boot_us, 0 = no pending restart). Set by
|
|
66
|
+
* MIK_Stop when an uncaught exception happens with a protocol REPL
|
|
67
|
+
* attached: the serve loop keeps reading commands until the deadline so
|
|
68
|
+
* the host can still deploy/clean/--recover during the grace window,
|
|
69
|
+
* then platform->restart() fires from MIK_Loop. */
|
|
70
|
+
int64_t restart_at_us;
|
|
59
71
|
const char* fs_base_path;
|
|
60
72
|
const char* fs_root; /* Sandbox root for mikrojs/fs operations (separate from module resolution) */
|
|
61
73
|
size_t fs_limit; /* Max bytes for fs_root (0 = unlimited) */
|
|
@@ -259,6 +271,9 @@ void mik__repl_set_paused(bool paused);
|
|
|
259
271
|
#define MIK_MSG_ERR 0x81
|
|
260
272
|
#define MIK_MSG_CHECKSUM_RESULT 0x82
|
|
261
273
|
#define MIK_MSG_CONFIG_ENTRIES 0x83
|
|
274
|
+
/* Chunk of file bytes streamed in response to MIK_CMD_FS_GET.
|
|
275
|
+
* Multiple frames precede a final MSG_OK signaling end-of-file. */
|
|
276
|
+
#define MIK_MSG_FS_CHUNK 0x84
|
|
262
277
|
|
|
263
278
|
/* CLI → Device command types */
|
|
264
279
|
#define MIK_CMD_EVAL 0x10
|
|
@@ -284,6 +299,10 @@ void mik__repl_set_paused(bool paused);
|
|
|
284
299
|
#define MIK_CMD_RESTART 0x28
|
|
285
300
|
#define MIK_CMD_RUNTIME_PAUSE 0x29
|
|
286
301
|
#define MIK_CMD_RUNTIME_RESUME 0x2A
|
|
302
|
+
/* Pull a file off the device. Payload: u16le path_len | path.
|
|
303
|
+
* Device replies with zero-or-more MIK_MSG_FS_CHUNK frames followed by
|
|
304
|
+
* MIK_MSG_OK on EOF, or MIK_MSG_ERR if the path can't be opened. */
|
|
305
|
+
#define MIK_CMD_FS_GET 0x2B
|
|
287
306
|
|
|
288
307
|
#define MIK_CMD_CONFIG_LIST 0x40
|
|
289
308
|
#define MIK_CMD_CONFIG_SET 0x41
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikrojs/native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0-pr-85.g6768f8c",
|
|
4
4
|
"description": "Mikro.js C++ runtime library and Node.js native addon",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"esp32",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"cmake-js": "^8.0.0",
|
|
79
79
|
"node-addon-api": "^8.7.0",
|
|
80
80
|
"node-gyp-build": "^4.8.4",
|
|
81
|
-
"@mikrojs/quickjs": "0.
|
|
81
|
+
"@mikrojs/quickjs": "0.8.0-pr-85.g6768f8c"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
84
|
"@swc/core": "^1.15.30",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/runtime/fs/fs.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {ok} from 'mikrojs/result'
|
|
1
|
+
import {err, ok} from 'mikrojs/result'
|
|
2
2
|
import * as native from 'native:fs'
|
|
3
3
|
|
|
4
4
|
import type {Result} from '../result/types.js'
|
|
@@ -61,28 +61,26 @@ export const exists = native.exists
|
|
|
61
61
|
export function readStream(
|
|
62
62
|
path: string,
|
|
63
63
|
options?: ReadStreamOptions,
|
|
64
|
-
): Result<AsyncIterable<Uint8Array
|
|
64
|
+
): Result<AsyncIterable<Result<Uint8Array, FSError>>, FSError> {
|
|
65
65
|
const openResult = native.open(path)
|
|
66
66
|
if (!openResult.ok) return openResult
|
|
67
67
|
const fh = openResult.value
|
|
68
68
|
const chunkSize = options?.chunkSize ?? 512
|
|
69
69
|
|
|
70
|
-
async function* iterate(): AsyncIterable<Uint8Array
|
|
70
|
+
async function* iterate(): AsyncIterable<Result<Uint8Array, FSError>> {
|
|
71
71
|
try {
|
|
72
72
|
while (true) {
|
|
73
73
|
const r = fh.read(chunkSize)
|
|
74
74
|
if (!r.ok) {
|
|
75
75
|
// Handle-level errors (BadFileDescriptor, Unknown) don't carry a
|
|
76
|
-
// path from the native side.
|
|
77
|
-
//
|
|
78
|
-
// path, so we decorate here to preserve the original readStream
|
|
79
|
-
// context.
|
|
76
|
+
// path from the native side. Path-aware variants are decorated
|
|
77
|
+
// here so consumers see the original readStream context.
|
|
80
78
|
const e = r.error
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
yield e.name === 'BadFileDescriptor' || e.name === 'Unknown' ? err(e) : err({...e, path})
|
|
80
|
+
return
|
|
83
81
|
}
|
|
84
82
|
if (r.value === undefined) break
|
|
85
|
-
yield r.value
|
|
83
|
+
yield ok(r.value)
|
|
86
84
|
}
|
|
87
85
|
} finally {
|
|
88
86
|
fh.close()
|
package/runtime/fs/types.ts
CHANGED
|
@@ -79,8 +79,8 @@ export interface ReadStreamOptions {
|
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* Open a file as a stream of byte chunks. Returns a Result because the
|
|
82
|
-
* initial open can fail; once iterating, mid-stream read errors
|
|
83
|
-
*
|
|
82
|
+
* initial open can fail; once iterating, mid-stream read errors arrive
|
|
83
|
+
* as a final `err(...)` item and end the stream.
|
|
84
84
|
*
|
|
85
85
|
* Compose with `mikrojs/stream` helpers (`decodeUtf8`, `splitLines`) for
|
|
86
86
|
* text/line streams. Closes the underlying handle on EOF, mid-stream
|
|
@@ -89,4 +89,4 @@ export interface ReadStreamOptions {
|
|
|
89
89
|
export declare function readStream(
|
|
90
90
|
path: string,
|
|
91
91
|
options?: ReadStreamOptions,
|
|
92
|
-
): Result<AsyncIterable<Uint8Array
|
|
92
|
+
): Result<AsyncIterable<Result<Uint8Array, FSError>>, FSError>
|
package/runtime/http/helpers.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// LTE modem over UART) implement the `Request` type directly and reuse
|
|
4
4
|
// `prepareBody` + `makeResponse` for the boring parts.
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import {err, ok} from 'mikrojs/result'
|
|
7
|
+
|
|
8
|
+
import type {Result} from '../result/types.js'
|
|
7
9
|
|
|
8
10
|
export interface RequestOptions {
|
|
9
11
|
method?: string
|
|
@@ -25,21 +27,24 @@ export interface Response {
|
|
|
25
27
|
headers: [string, string][]
|
|
26
28
|
ok: boolean
|
|
27
29
|
/**
|
|
28
|
-
* Response body as an async iterable of chunks.
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
30
|
+
* Response body as an async iterable of byte chunks. Iteration yields
|
|
31
|
+
* `Result<Uint8Array, RequestError>` so mid-stream failures (network
|
|
32
|
+
* drop, abort, timeout) compose with the rest of the Result-based API.
|
|
33
|
+
* Single-shot: after any of `body`, `text()`, `bytes()`, or `json()`
|
|
34
|
+
* has started draining, the next consumer call throws
|
|
35
|
+
* `BodyConsumedError`.
|
|
31
36
|
*/
|
|
32
|
-
body: AsyncIterable<Uint8Array
|
|
37
|
+
body: AsyncIterable<Result<Uint8Array, RequestError>>
|
|
33
38
|
/** First matching header value, case-insensitive, or undefined if absent. */
|
|
34
39
|
get(name: string): string | undefined
|
|
35
40
|
/** All matching header values in order, case-insensitive. Empty array if absent. */
|
|
36
41
|
getAll(name: string): string[]
|
|
37
42
|
/** Drain body as a UTF-8 string. Throws `BodyConsumedError` on second consumer call. */
|
|
38
|
-
text(): Promise<string
|
|
43
|
+
text(): Promise<Result<string, RequestError>>
|
|
39
44
|
/** Drain body and parse as JSON. Throws `BodyConsumedError` on second consumer call. */
|
|
40
|
-
json(): Promise<unknown
|
|
45
|
+
json(): Promise<Result<unknown, RequestError>>
|
|
41
46
|
/** Drain body to a single `Uint8Array`. Throws `BodyConsumedError` on second consumer call. */
|
|
42
|
-
bytes(): Promise<Uint8Array
|
|
47
|
+
bytes(): Promise<Result<Uint8Array, RequestError>>
|
|
43
48
|
/**
|
|
44
49
|
* Cancel the request and release any native resources. Safe to call after
|
|
45
50
|
* the body has been consumed (no-op). Safe to call multiple times.
|
|
@@ -63,16 +68,11 @@ export const RequestError = {
|
|
|
63
68
|
Aborted: (message: string) => ({name: 'Aborted', message}) as const,
|
|
64
69
|
/** Transport is at its concurrent-request cap. Retry after in-flight requests settle. */
|
|
65
70
|
TooManyPending: () => ({name: 'TooManyPending'}) as const,
|
|
71
|
+
/** Body drained but JSON.parse rejected the payload. */
|
|
72
|
+
InvalidJson: (message: string) => ({name: 'InvalidJson', message}) as const,
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
export type RequestError =
|
|
69
|
-
| ReturnType<typeof RequestError.Hardware>
|
|
70
|
-
| ReturnType<typeof RequestError.Network>
|
|
71
|
-
| ReturnType<typeof RequestError.Timeout>
|
|
72
|
-
| ReturnType<typeof RequestError.BodyTooLarge>
|
|
73
|
-
| ReturnType<typeof RequestError.InvalidResponse>
|
|
74
|
-
| ReturnType<typeof RequestError.Aborted>
|
|
75
|
-
| ReturnType<typeof RequestError.TooManyPending>
|
|
75
|
+
export type RequestError = ReturnType<(typeof RequestError)[keyof typeof RequestError]>
|
|
76
76
|
|
|
77
77
|
export class BodyConsumedError extends Error {
|
|
78
78
|
readonly name = 'BodyConsumed'
|
|
@@ -99,9 +99,9 @@ export function prepareBody(opts: RequestOptions): {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* Wrap a transport's raw response (async-iterable body) into
|
|
103
|
-
* `Response` shape with `text()`/`json()`/`bytes()`/`get()
|
|
104
|
-
* and a single-shot body claim.
|
|
102
|
+
* Wrap a transport's raw response (Result-yielding async-iterable body) into
|
|
103
|
+
* the public `Response` shape with `text()`/`json()`/`bytes()`/`get()`/
|
|
104
|
+
* `getAll()`/`close()` and a single-shot body claim.
|
|
105
105
|
*/
|
|
106
106
|
export function makeResponse(raw: {
|
|
107
107
|
status: number
|
|
@@ -109,7 +109,7 @@ export function makeResponse(raw: {
|
|
|
109
109
|
url: string
|
|
110
110
|
redirected: boolean
|
|
111
111
|
headers: [string, string][]
|
|
112
|
-
body: AsyncIterable<Uint8Array
|
|
112
|
+
body: AsyncIterable<Result<Uint8Array, RequestError>>
|
|
113
113
|
}): Response {
|
|
114
114
|
let consumed = false
|
|
115
115
|
const claim = () => {
|
|
@@ -117,7 +117,7 @@ export function makeResponse(raw: {
|
|
|
117
117
|
consumed = true
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
const body: AsyncIterable<Uint8Array
|
|
120
|
+
const body: AsyncIterable<Result<Uint8Array, RequestError>> = {
|
|
121
121
|
[Symbol.asyncIterator]() {
|
|
122
122
|
claim()
|
|
123
123
|
return raw.body[Symbol.asyncIterator]()
|
|
@@ -153,7 +153,13 @@ export function makeResponse(raw: {
|
|
|
153
153
|
},
|
|
154
154
|
json: async () => {
|
|
155
155
|
claim()
|
|
156
|
-
|
|
156
|
+
const text = await drainAsText(raw.body)
|
|
157
|
+
if (!text.ok) return text
|
|
158
|
+
try {
|
|
159
|
+
return ok(JSON.parse(text.value))
|
|
160
|
+
} catch (e) {
|
|
161
|
+
return err(RequestError.InvalidJson(e instanceof Error ? e.message : String(e)))
|
|
162
|
+
}
|
|
157
163
|
},
|
|
158
164
|
bytes: async () => {
|
|
159
165
|
claim()
|
|
@@ -177,29 +183,35 @@ function normalizeHeaders(
|
|
|
177
183
|
return out
|
|
178
184
|
}
|
|
179
185
|
|
|
180
|
-
async function drain(
|
|
186
|
+
async function drain(
|
|
187
|
+
source: AsyncIterable<Result<Uint8Array, RequestError>>,
|
|
188
|
+
): Promise<Result<Uint8Array, RequestError>> {
|
|
181
189
|
const parts: Uint8Array[] = []
|
|
182
190
|
let total = 0
|
|
183
|
-
for await (const
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
for await (const r of source) {
|
|
192
|
+
if (!r.ok) return r
|
|
193
|
+
parts.push(r.value)
|
|
194
|
+
total += r.value.length
|
|
186
195
|
}
|
|
187
|
-
if (parts.length === 1) return parts[0]!
|
|
196
|
+
if (parts.length === 1) return ok(parts[0]!)
|
|
188
197
|
const out = new Uint8Array(total)
|
|
189
198
|
let offset = 0
|
|
190
199
|
for (const p of parts) {
|
|
191
200
|
out.set(p, offset)
|
|
192
201
|
offset += p.length
|
|
193
202
|
}
|
|
194
|
-
return out
|
|
203
|
+
return ok(out)
|
|
195
204
|
}
|
|
196
205
|
|
|
197
|
-
async function drainAsText(
|
|
206
|
+
async function drainAsText(
|
|
207
|
+
source: AsyncIterable<Result<Uint8Array, RequestError>>,
|
|
208
|
+
): Promise<Result<string, RequestError>> {
|
|
198
209
|
const decoder = new TextDecoder()
|
|
199
210
|
let out = ''
|
|
200
|
-
for await (const
|
|
201
|
-
|
|
211
|
+
for await (const r of source) {
|
|
212
|
+
if (!r.ok) return r
|
|
213
|
+
out += decoder.decode(r.value, {stream: true})
|
|
202
214
|
}
|
|
203
215
|
out += decoder.decode()
|
|
204
|
-
return out
|
|
216
|
+
return ok(out)
|
|
205
217
|
}
|
package/runtime/http/native.ts
CHANGED
|
@@ -92,10 +92,10 @@ export function createRequestFromNative(native: NativeHttpModule): Request {
|
|
|
92
92
|
const {status, headers: responseHeaders} = headersResult.value
|
|
93
93
|
|
|
94
94
|
let done = false
|
|
95
|
-
const rawBody: AsyncIterable<Uint8Array
|
|
95
|
+
const rawBody: AsyncIterable<Result<Uint8Array, RequestError>> = {
|
|
96
96
|
[Symbol.asyncIterator]() {
|
|
97
97
|
return {
|
|
98
|
-
async next() {
|
|
98
|
+
async next(): Promise<IteratorResult<Result<Uint8Array, RequestError>>> {
|
|
99
99
|
if (done) return {done: true, value: undefined}
|
|
100
100
|
let msg
|
|
101
101
|
try {
|
|
@@ -103,19 +103,28 @@ export function createRequestFromNative(native: NativeHttpModule): Request {
|
|
|
103
103
|
} catch (e) {
|
|
104
104
|
done = true
|
|
105
105
|
cleanup()
|
|
106
|
-
|
|
106
|
+
const message = e instanceof Error ? e.message : String(e)
|
|
107
|
+
return {done: false, value: err(RequestError.Network(message))}
|
|
107
108
|
}
|
|
108
109
|
if (msg.kind === 'chunk') {
|
|
109
|
-
return {done: false, value: msg.data}
|
|
110
|
+
return {done: false, value: ok(msg.data)}
|
|
110
111
|
}
|
|
111
112
|
done = true
|
|
112
113
|
cleanup()
|
|
113
114
|
if (msg.kind === 'end') return {done: true, value: undefined}
|
|
114
115
|
/* error */
|
|
115
116
|
if (msg.cancelled) {
|
|
116
|
-
|
|
117
|
+
return {
|
|
118
|
+
done: false,
|
|
119
|
+
value: err(
|
|
120
|
+
RequestError.Aborted(msg.message || String(options.signal?.reason ?? 'aborted')),
|
|
121
|
+
),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
done: false,
|
|
126
|
+
value: err(RequestError.Network(msg.message || 'HTTP request failed')),
|
|
117
127
|
}
|
|
118
|
-
throw RequestError.Network(msg.message || 'HTTP request failed')
|
|
119
128
|
},
|
|
120
129
|
async return() {
|
|
121
130
|
if (!done) {
|
package/runtime/internal.d.ts
CHANGED
|
@@ -318,8 +318,8 @@ declare module 'native:uart' {
|
|
|
318
318
|
|
|
319
319
|
read(): Result<
|
|
320
320
|
{
|
|
321
|
-
next(): Promise<IteratorResult<Uint8Array
|
|
322
|
-
return(): Promise<IteratorResult<Uint8Array
|
|
321
|
+
next(): Promise<IteratorResult<Result<Uint8Array, UartError>>>
|
|
322
|
+
return(): Promise<IteratorResult<Result<Uint8Array, UartError>>>
|
|
323
323
|
},
|
|
324
324
|
UartError
|
|
325
325
|
>
|
package/runtime/kv/shared.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {err, ok} from 'mikrojs/result'
|
|
2
2
|
// Typed loosely to avoid deep type instantiation from Infer<S> in parse's generics.
|
|
3
3
|
// Type safety is provided by the storage interface overloads in types.ts.
|
|
4
4
|
import {parse as _parse} from 'mikrojs/schema'
|
|
@@ -8,12 +8,14 @@ import type {NativeError} from '../result/types.js'
|
|
|
8
8
|
type ParseResult = {ok: true; value: unknown} | {ok: false; error: {message: string; path: string}}
|
|
9
9
|
const parse = _parse as unknown as (schema: unknown, value: unknown) => ParseResult
|
|
10
10
|
|
|
11
|
-
export const KVError =
|
|
12
|
-
StorageFull: (message: string) => ({message}),
|
|
13
|
-
EncodeFailed: (message: string) => ({message}),
|
|
14
|
-
WriteFailed: (message: string) => ({message}),
|
|
15
|
-
ValidationFailed: (message: string, path: string) =>
|
|
16
|
-
})
|
|
11
|
+
export const KVError = {
|
|
12
|
+
StorageFull: (message: string) => ({name: 'StorageFull', message}) as const,
|
|
13
|
+
EncodeFailed: (message: string) => ({name: 'EncodeFailed', message}) as const,
|
|
14
|
+
WriteFailed: (message: string) => ({name: 'WriteFailed', message}) as const,
|
|
15
|
+
ValidationFailed: (message: string, path: string) =>
|
|
16
|
+
({name: 'ValidationFailed', message, path}) as const,
|
|
17
|
+
Unknown: (code: number, message: string) => ({name: 'Unknown', code, message}) as const,
|
|
18
|
+
}
|
|
17
19
|
|
|
18
20
|
/* Error codes from errors.h (MIK_ERR_BASE + 0xD0..0xD4) */
|
|
19
21
|
const KV_INVALID_KEY = 0x80d0
|
|
@@ -43,7 +45,7 @@ function mapKvError(e: NativeError) {
|
|
|
43
45
|
case KV_WRITE:
|
|
44
46
|
return KVError.WriteFailed(e.message)
|
|
45
47
|
default:
|
|
46
|
-
|
|
48
|
+
return KVError.Unknown(e.code, e.message)
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
|
package/runtime/kv/types.ts
CHANGED
|
@@ -55,6 +55,7 @@ export type KVError =
|
|
|
55
55
|
| {name: 'EncodeFailed'; message: string}
|
|
56
56
|
| {name: 'WriteFailed'; message: string}
|
|
57
57
|
| {name: 'ValidationFailed'; message: string; path: string}
|
|
58
|
+
| {name: 'Unknown'; code: number; message: string}
|
|
58
59
|
|
|
59
60
|
export type KVOptions<S> =
|
|
60
61
|
| {
|
package/runtime/reader/reader.ts
CHANGED
|
@@ -9,12 +9,19 @@
|
|
|
9
9
|
* lazy-load efficiency.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import {err, ok} from 'mikrojs/result'
|
|
13
|
+
|
|
14
|
+
import type {Result} from '../result/types.js'
|
|
15
|
+
|
|
16
|
+
export type ReaderError = {name: 'Timeout'; ms: number} | {name: 'StreamClosed'}
|
|
17
|
+
|
|
12
18
|
/**
|
|
13
|
-
* A buffered byte reader over
|
|
19
|
+
* A buffered byte reader over a Result-yielding async iterator of
|
|
20
|
+
* byte chunks.
|
|
14
21
|
*
|
|
15
|
-
* Wraps any `AsyncIterable<Uint8Array
|
|
16
|
-
*
|
|
17
|
-
* share the same underlying byte buffer:
|
|
22
|
+
* Wraps any `AsyncIterable<Result<Uint8Array, E>>` (typically
|
|
23
|
+
* `uart.read()`, `fs.readStream`, an HTTP body) and exposes three
|
|
24
|
+
* primitives that share the same underlying byte buffer:
|
|
18
25
|
*
|
|
19
26
|
* - `readUntil(delimiter, {timeoutMs})` — pull chunks until the
|
|
20
27
|
* buffer contains `delimiter`, return everything before it and
|
|
@@ -30,12 +37,12 @@
|
|
|
30
37
|
* underlying iterator. Useful before sending a new command when
|
|
31
38
|
* stale bytes from a previous response might be left over.
|
|
32
39
|
*
|
|
33
|
-
* Both `readUntil` and `readBytes`
|
|
34
|
-
*
|
|
35
|
-
* the request is satisfied,
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
40
|
+
* Both `readUntil` and `readBytes` return `Result<Uint8Array, E |
|
|
41
|
+
* ReaderError>`: `err({name: 'Timeout', ms})` if the deadline expires
|
|
42
|
+
* before the request is satisfied, `err({name: 'StreamClosed'})` if
|
|
43
|
+
* the underlying iterator finishes early, or whatever error variant
|
|
44
|
+
* the source yields. `RangeError` is still thrown for invalid
|
|
45
|
+
* arguments (programmer mistakes, not stream errors).
|
|
39
46
|
*
|
|
40
47
|
* Timeout semantics: when a pull races against a `setTimeout` and
|
|
41
48
|
* the timer wins, the pending `iter.next()` promise is kept so the
|
|
@@ -48,18 +55,21 @@
|
|
|
48
55
|
* not protected against — the caller is responsible for
|
|
49
56
|
* sequential ordering.
|
|
50
57
|
*/
|
|
51
|
-
export class BufferedReader {
|
|
52
|
-
readonly #source: AsyncIterator<Uint8Array
|
|
58
|
+
export class BufferedReader<E = never> {
|
|
59
|
+
readonly #source: AsyncIterator<Result<Uint8Array, E>>
|
|
53
60
|
#buffer: Uint8Array
|
|
54
|
-
#pending: Promise<IteratorResult<Uint8Array
|
|
61
|
+
#pending: Promise<IteratorResult<Result<Uint8Array, E>>> | null
|
|
55
62
|
|
|
56
|
-
constructor(source: AsyncIterable<Uint8Array
|
|
57
|
-
this.#source = source[Symbol.asyncIterator]() as AsyncIterator<Uint8Array
|
|
63
|
+
constructor(source: AsyncIterable<Result<Uint8Array, E>>) {
|
|
64
|
+
this.#source = source[Symbol.asyncIterator]() as AsyncIterator<Result<Uint8Array, E>>
|
|
58
65
|
this.#buffer = new Uint8Array(0)
|
|
59
66
|
this.#pending = null
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
async readUntil(
|
|
69
|
+
async readUntil(
|
|
70
|
+
delimiter: Uint8Array,
|
|
71
|
+
options: {timeoutMs: number},
|
|
72
|
+
): Promise<Result<Uint8Array, E | ReaderError>> {
|
|
63
73
|
if (delimiter.length === 0) {
|
|
64
74
|
throw new RangeError('BufferedReader.readUntil: delimiter must be non-empty')
|
|
65
75
|
}
|
|
@@ -69,24 +79,29 @@ export class BufferedReader {
|
|
|
69
79
|
if (idx !== -1) {
|
|
70
80
|
const out = this.#buffer.slice(0, idx)
|
|
71
81
|
this.#buffer = this.#buffer.slice(idx + delimiter.length)
|
|
72
|
-
return out
|
|
82
|
+
return ok(out)
|
|
73
83
|
}
|
|
74
|
-
await this.#pull(deadline)
|
|
84
|
+
const pull = await this.#pull(deadline, options.timeoutMs)
|
|
85
|
+
if (!pull.ok) return pull
|
|
75
86
|
}
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
async readBytes(
|
|
89
|
+
async readBytes(
|
|
90
|
+
count: number,
|
|
91
|
+
options: {timeoutMs: number},
|
|
92
|
+
): Promise<Result<Uint8Array, E | ReaderError>> {
|
|
79
93
|
if (count < 0) {
|
|
80
94
|
throw new RangeError('BufferedReader.readBytes: count must be non-negative')
|
|
81
95
|
}
|
|
82
|
-
if (count === 0) return new Uint8Array(0)
|
|
96
|
+
if (count === 0) return ok(new Uint8Array(0))
|
|
83
97
|
const deadline = Date.now() + options.timeoutMs
|
|
84
98
|
while (this.#buffer.length < count) {
|
|
85
|
-
await this.#pull(deadline)
|
|
99
|
+
const pull = await this.#pull(deadline, options.timeoutMs)
|
|
100
|
+
if (!pull.ok) return pull
|
|
86
101
|
}
|
|
87
102
|
const out = this.#buffer.slice(0, count)
|
|
88
103
|
this.#buffer = this.#buffer.slice(count)
|
|
89
|
-
return out
|
|
104
|
+
return ok(out)
|
|
90
105
|
}
|
|
91
106
|
|
|
92
107
|
/**
|
|
@@ -100,13 +115,9 @@ export class BufferedReader {
|
|
|
100
115
|
this.#buffer = new Uint8Array(0)
|
|
101
116
|
}
|
|
102
117
|
|
|
103
|
-
async #pull(deadline: number): Promise<void
|
|
118
|
+
async #pull(deadline: number, timeoutMs: number): Promise<Result<void, E | ReaderError>> {
|
|
104
119
|
const remaining = deadline - Date.now()
|
|
105
|
-
if (remaining <= 0) {
|
|
106
|
-
const err = new Error(`BufferedReader: timeout waiting for bytes`)
|
|
107
|
-
err.name = 'TimeoutError'
|
|
108
|
-
throw err
|
|
109
|
-
}
|
|
120
|
+
if (remaining <= 0) return err({name: 'Timeout' as const, ms: timeoutMs})
|
|
110
121
|
|
|
111
122
|
if (this.#pending === null) {
|
|
112
123
|
this.#pending = this.#source.next()
|
|
@@ -116,24 +127,24 @@ export class BufferedReader {
|
|
|
116
127
|
let timerId: ReturnType<typeof setTimeout> | undefined
|
|
117
128
|
try {
|
|
118
129
|
const result = await Promise.race([
|
|
119
|
-
pending.then((v): {kind: 'value'; v: IteratorResult<Uint8Array
|
|
130
|
+
pending.then((v): {kind: 'value'; v: IteratorResult<Result<Uint8Array, E>>} => ({
|
|
131
|
+
kind: 'value',
|
|
132
|
+
v,
|
|
133
|
+
})),
|
|
120
134
|
new Promise<{kind: 'timeout'}>((resolve) => {
|
|
121
135
|
timerId = setTimeout(() => resolve({kind: 'timeout'}), remaining)
|
|
122
136
|
}),
|
|
123
137
|
])
|
|
124
138
|
if (result.kind === 'timeout') {
|
|
125
139
|
// Leave #pending intact so the next pull resumes on this chunk.
|
|
126
|
-
|
|
127
|
-
err.name = 'TimeoutError'
|
|
128
|
-
throw err
|
|
140
|
+
return err({name: 'Timeout' as const, ms: timeoutMs})
|
|
129
141
|
}
|
|
130
142
|
this.#pending = null
|
|
131
|
-
if (result.v.done) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.#buffer = concatBytes(this.#buffer, result.v.value)
|
|
143
|
+
if (result.v.done) return err({name: 'StreamClosed' as const})
|
|
144
|
+
const item = result.v.value
|
|
145
|
+
if (!item.ok) return item
|
|
146
|
+
this.#buffer = concatBytes(this.#buffer, item.value)
|
|
147
|
+
return ok()
|
|
137
148
|
} finally {
|
|
138
149
|
if (timerId !== undefined) clearTimeout(timerId)
|
|
139
150
|
}
|
package/runtime/reader/types.ts
CHANGED
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
* `packages/mikrojs` and by user apps via their tsconfig.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type {Result} from 'mikrojs/result'
|
|
11
|
+
|
|
12
|
+
export type ReaderError = {name: 'Timeout'; ms: number} | {name: 'StreamClosed'}
|
|
13
|
+
|
|
10
14
|
/**
|
|
11
|
-
* A buffered byte reader over
|
|
12
|
-
* chunks. Offers three operations that share the same
|
|
13
|
-
* buffer:
|
|
15
|
+
* A buffered byte reader over a Result-yielding async iterator of
|
|
16
|
+
* `Uint8Array` chunks. Offers three operations that share the same
|
|
17
|
+
* underlying buffer:
|
|
14
18
|
*
|
|
15
19
|
* - `readUntil(delimiter, {timeoutMs})` — return everything before
|
|
16
20
|
* the first occurrence of `delimiter`, consume the delimiter.
|
|
@@ -19,16 +23,23 @@
|
|
|
19
23
|
* - `drain()` — discard buffered bytes without affecting the
|
|
20
24
|
* underlying iterator.
|
|
21
25
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
26
|
+
* Returns `err({name: 'Timeout', ms})` if the deadline expires,
|
|
27
|
+
* `err({name: 'StreamClosed'})` if the underlying iterator finishes
|
|
28
|
+
* early, or the source's own error variant when it yields one.
|
|
29
|
+
* `RangeError` is still thrown for invalid arguments.
|
|
25
30
|
*
|
|
26
31
|
* Single-consumer only: concurrent `readUntil` / `readBytes` calls on
|
|
27
32
|
* the same instance are unsupported.
|
|
28
33
|
*/
|
|
29
|
-
export declare class BufferedReader {
|
|
30
|
-
constructor(source: AsyncIterable<Uint8Array
|
|
31
|
-
readUntil(
|
|
32
|
-
|
|
34
|
+
export declare class BufferedReader<E = never> {
|
|
35
|
+
constructor(source: AsyncIterable<Result<Uint8Array, E>>)
|
|
36
|
+
readUntil(
|
|
37
|
+
delimiter: Uint8Array,
|
|
38
|
+
options: {timeoutMs: number},
|
|
39
|
+
): Promise<Result<Uint8Array, E | ReaderError>>
|
|
40
|
+
readBytes(
|
|
41
|
+
count: number,
|
|
42
|
+
options: {timeoutMs: number},
|
|
43
|
+
): Promise<Result<Uint8Array, E | ReaderError>>
|
|
33
44
|
drain(): void
|
|
34
45
|
}
|
package/runtime/result/result.ts
CHANGED
|
@@ -7,20 +7,9 @@ export class PanicError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
):
|
|
14
|
-
|
|
15
|
-
for (const key in variants) {
|
|
16
|
-
const factory = variants[key]!
|
|
17
|
-
constructors[key] = (...args: unknown[]) => {
|
|
18
|
-
const fields = (factory as (...a: unknown[]) => Record<string, unknown>)(...args)
|
|
19
|
-
fields.name = key
|
|
20
|
-
return fields
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return constructors as unknown as {
|
|
24
|
-
[K in keyof D & string]: (...args: Parameters<D[K]>) => {name: K} & ReturnType<D[K]>
|
|
25
|
-
}
|
|
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
|
+
return (handlers as unknown as Record<string, (e: E) => R>)[error.name]!(error)
|
|
26
15
|
}
|
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
|
|
@@ -106,28 +119,30 @@ export async function collectUntil<T>(
|
|
|
106
119
|
* timeout, or consumer break) so underlying resources (UART claims,
|
|
107
120
|
* socket handles, ring buffers) get released.
|
|
108
121
|
*/
|
|
109
|
-
export async function* withTimeout<T>(
|
|
122
|
+
export async function* withTimeout<T, E>(
|
|
123
|
+
source: AsyncIterable<Result<T, E>>,
|
|
124
|
+
ms: number,
|
|
125
|
+
): AsyncIterable<Result<T, E | {name: 'Timeout'; ms: number}>> {
|
|
110
126
|
const deadline = Date.now() + ms
|
|
111
127
|
const iter = source[Symbol.asyncIterator]()
|
|
112
128
|
try {
|
|
113
129
|
while (true) {
|
|
114
130
|
const remaining = deadline - Date.now()
|
|
115
131
|
if (remaining <= 0) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
throw err
|
|
132
|
+
yield err({name: 'Timeout' as const, ms})
|
|
133
|
+
return
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
let timerId: ReturnType<typeof setTimeout> | undefined
|
|
122
|
-
let result: IteratorResult<T, unknown>
|
|
137
|
+
let result: IteratorResult<Result<T, E>, unknown>
|
|
138
|
+
let timedOut = false
|
|
123
139
|
try {
|
|
124
140
|
result = await Promise.race([
|
|
125
141
|
iter.next(),
|
|
126
|
-
new Promise<IteratorResult<T, unknown>>((
|
|
142
|
+
new Promise<IteratorResult<Result<T, E>, unknown>>((resolve) => {
|
|
127
143
|
timerId = setTimeout(() => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
reject(err)
|
|
144
|
+
timedOut = true
|
|
145
|
+
resolve({done: true, value: undefined})
|
|
131
146
|
}, remaining)
|
|
132
147
|
}),
|
|
133
148
|
])
|
|
@@ -135,8 +150,13 @@ export async function* withTimeout<T>(source: AsyncIterable<T>, ms: number): Asy
|
|
|
135
150
|
if (timerId !== undefined) clearTimeout(timerId)
|
|
136
151
|
}
|
|
137
152
|
|
|
153
|
+
if (timedOut) {
|
|
154
|
+
yield err({name: 'Timeout' as const, ms})
|
|
155
|
+
return
|
|
156
|
+
}
|
|
138
157
|
if (result.done) return
|
|
139
|
-
yield result.value
|
|
158
|
+
yield result.value
|
|
159
|
+
if (!result.value.ok) return
|
|
140
160
|
}
|
|
141
161
|
} finally {
|
|
142
162
|
if (typeof iter.return === 'function') {
|
package/runtime/stream/types.ts
CHANGED
|
@@ -5,42 +5,56 @@
|
|
|
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.
|
|
42
53
|
*/
|
|
43
|
-
export declare function withTimeout<T>(
|
|
54
|
+
export declare function withTimeout<T, E>(
|
|
55
|
+
source: AsyncIterable<Result<T, E>>,
|
|
56
|
+
ms: number,
|
|
57
|
+
): AsyncIterable<Result<T, E | Extract<StreamError, {name: 'Timeout'}>>>
|
|
44
58
|
|
|
45
59
|
/* BufferedReader lives in the sibling `mikrojs/reader` module — see
|
|
46
60
|
* `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
|
}
|
|
@@ -104,7 +127,13 @@ bool mik__proto_read_exact(MIKReplTransport* transport, void* buf, size_t n) {
|
|
|
104
127
|
return false;
|
|
105
128
|
}
|
|
106
129
|
}
|
|
107
|
-
|
|
130
|
+
/* Microtasks are deferred user JS: settling them re-enters .then
|
|
131
|
+
* handlers that can write to the transport (console.log → MSG_LOG)
|
|
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)) {
|
|
108
137
|
mik__execute_jobs(repl_ctx);
|
|
109
138
|
}
|
|
110
139
|
/* A pumped job (e.g. the test runtime's __testFileDone) may
|
|
@@ -1109,12 +1138,14 @@ void MIK_ProtocolServeLoop(void) {
|
|
|
1109
1138
|
}
|
|
1110
1139
|
}
|
|
1111
1140
|
|
|
1112
|
-
/* Pump event loop between commands
|
|
1113
|
-
*
|
|
1141
|
+
/* Pump event loop between commands. Skip both timers/callbacks AND
|
|
1142
|
+
* microtasks when paused — draining microtasks runs user .then
|
|
1143
|
+
* handlers, which can interleave MSG_LOG output with deploy traffic
|
|
1144
|
+
* or race the staged-file swap. */
|
|
1114
1145
|
if (repl_mik_rt && !repl_paused) {
|
|
1115
1146
|
MIK_Loop(repl_mik_rt);
|
|
1116
1147
|
}
|
|
1117
|
-
if (ctx) {
|
|
1148
|
+
if (ctx && !repl_paused) {
|
|
1118
1149
|
mik__execute_jobs(ctx);
|
|
1119
1150
|
}
|
|
1120
1151
|
}
|
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
|
|