@mikrojs/native 0.6.0 → 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 +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.7.0",
|
|
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.7.0"
|
|
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
|
}
|