@mikrojs/native 0.0.7

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.
Files changed (109) hide show
  1. package/CMakeLists.txt +198 -0
  2. package/LICENSE +21 -0
  3. package/README.md +49 -0
  4. package/cmake/mikrojs_bytecode.cmake +146 -0
  5. package/cmake.js +22 -0
  6. package/dist/index.d.ts +52 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +132 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/types.d.ts +43 -0
  11. package/dist/types.d.ts.map +1 -0
  12. package/dist/types.js +2 -0
  13. package/dist/types.js.map +1 -0
  14. package/include/byteorder_apple.h +11 -0
  15. package/include/byteorder_windows.h +12 -0
  16. package/include/mikrojs/cbor_helpers.h +24 -0
  17. package/include/mikrojs/cutils_wrap.h +59 -0
  18. package/include/mikrojs/errors.h +144 -0
  19. package/include/mikrojs/mem.h +11 -0
  20. package/include/mikrojs/mik_color.h +32 -0
  21. package/include/mikrojs/mikrojs.h +331 -0
  22. package/include/mikrojs/platform.h +82 -0
  23. package/include/mikrojs/private.h +281 -0
  24. package/include/mikrojs/utils.h +125 -0
  25. package/package.json +100 -0
  26. package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
  27. package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
  28. package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
  29. package/runtime/ble/ble.ts +231 -0
  30. package/runtime/ble/types.ts +194 -0
  31. package/runtime/ble/uuid.ts +89 -0
  32. package/runtime/ble/validators.ts +61 -0
  33. package/runtime/cbor/cbor.ts +1 -0
  34. package/runtime/cbor/types.ts +8 -0
  35. package/runtime/console/types.ts +50 -0
  36. package/runtime/env/env.ts +17 -0
  37. package/runtime/env/types.ts +12 -0
  38. package/runtime/format/types.ts +4 -0
  39. package/runtime/fs/fs.ts +93 -0
  40. package/runtime/fs/types.ts +92 -0
  41. package/runtime/globals.d.ts +87 -0
  42. package/runtime/http/helpers.ts +222 -0
  43. package/runtime/http/native.ts +151 -0
  44. package/runtime/http/request.ts +25 -0
  45. package/runtime/i2c/i2c.ts +35 -0
  46. package/runtime/i2c/types.ts +55 -0
  47. package/runtime/inspect/types.ts +10 -0
  48. package/runtime/internal.d.ts +456 -0
  49. package/runtime/kv/nvs.ts +17 -0
  50. package/runtime/kv/rtc.ts +17 -0
  51. package/runtime/kv/shared.ts +107 -0
  52. package/runtime/kv/types.ts +150 -0
  53. package/runtime/neopixel/neopixel.ts +38 -0
  54. package/runtime/neopixel/types.ts +27 -0
  55. package/runtime/pin/pin.ts +51 -0
  56. package/runtime/pin/types.ts +49 -0
  57. package/runtime/pwm/pwm.ts +32 -0
  58. package/runtime/pwm/types.ts +29 -0
  59. package/runtime/reader/reader.ts +167 -0
  60. package/runtime/reader/types.ts +34 -0
  61. package/runtime/result/native-result.node-shim.ts +44 -0
  62. package/runtime/result/result.ts +26 -0
  63. package/runtime/result/types.ts +60 -0
  64. package/runtime/schema/schema.ts +321 -0
  65. package/runtime/schema/types.ts +152 -0
  66. package/runtime/sleep/sleep.ts +14 -0
  67. package/runtime/sleep/types.ts +44 -0
  68. package/runtime/sntp/sntp.ts +54 -0
  69. package/runtime/sntp/types.ts +38 -0
  70. package/runtime/spi/spi.ts +31 -0
  71. package/runtime/spi/types.ts +42 -0
  72. package/runtime/stdio/stdio.ts +44 -0
  73. package/runtime/stdio/types.ts +22 -0
  74. package/runtime/stream/stream.ts +150 -0
  75. package/runtime/stream/types.ts +47 -0
  76. package/runtime/sys/sys.ts +90 -0
  77. package/runtime/sys/types.ts +131 -0
  78. package/runtime/test/test.ts +595 -0
  79. package/runtime/test/types.ts +97 -0
  80. package/runtime/uart/types.ts +75 -0
  81. package/runtime/uart/uart.ts +51 -0
  82. package/runtime/wifi/types.ts +156 -0
  83. package/runtime/wifi/wifi.ts +208 -0
  84. package/scripts/bundle-runtime.js +149 -0
  85. package/scripts/compare-minifiers.js +189 -0
  86. package/scripts/compile-bytecode.sh +38 -0
  87. package/scripts/copy-prebuild.js +20 -0
  88. package/scripts/generate-symbol-map.js +146 -0
  89. package/src/builtins.cpp +82 -0
  90. package/src/cutils_compat.c +38 -0
  91. package/src/eval_bytecode.cpp +42 -0
  92. package/src/fs.cpp +878 -0
  93. package/src/mem.cpp +63 -0
  94. package/src/mik_abort.cpp +160 -0
  95. package/src/mik_app_config.cpp +358 -0
  96. package/src/mik_cbor.cpp +334 -0
  97. package/src/mik_color.cpp +46 -0
  98. package/src/mik_console.cpp +422 -0
  99. package/src/mik_inspect.cpp +850 -0
  100. package/src/mik_repl.cpp +1122 -0
  101. package/src/mik_result.cpp +344 -0
  102. package/src/mik_stdio.cpp +147 -0
  103. package/src/mik_sys.cpp +239 -0
  104. package/src/mik_text_encoding.cpp +443 -0
  105. package/src/mikrojs.cpp +942 -0
  106. package/src/modules.cpp +944 -0
  107. package/src/platform_posix.cpp +134 -0
  108. package/src/timers.cpp +208 -0
  109. package/src/utils.cpp +173 -0
@@ -0,0 +1,93 @@
1
+ import {ok} from 'mikrojs/result'
2
+ import * as native from 'native:fs'
3
+
4
+ import type {Result} from '../result/types.js'
5
+ import type {
6
+ DirEntry,
7
+ FSError,
8
+ MkdirOptions,
9
+ ReadStreamOptions,
10
+ StatResult,
11
+ WriteFileOptions,
12
+ } from './types.js'
13
+
14
+ // native:fs emits typed FSError variants directly (see mik__fs_err_result in
15
+ // src/fs.cpp), including the `path` field for path-level operations. These
16
+ // wrappers are thin pass-throughs that only exist to expose a typed public
17
+ // API surface for mikrojs/fs.
18
+
19
+ export function readFile(path: string): Result<Uint8Array, FSError>
20
+ export function readFile(path: string, encoding: 'utf-8'): Result<string, FSError>
21
+ export function readFile(path: string, encoding?: 'utf-8'): Result<Uint8Array | string, FSError> {
22
+ return encoding ? native.readFile(path, encoding) : native.readFile(path)
23
+ }
24
+
25
+ export function writeFile(
26
+ path: string,
27
+ contents: string | Uint8Array,
28
+ options?: WriteFileOptions,
29
+ ): Result<void, FSError> {
30
+ return typeof contents === 'string'
31
+ ? native.writeFile(path, contents, options)
32
+ : native.writeFile(path, contents, options)
33
+ }
34
+
35
+ export function stat(path: string): Result<StatResult, FSError> {
36
+ return native.stat(path)
37
+ }
38
+
39
+ export function readDir(path: string): Result<DirEntry[], FSError> {
40
+ return native.readDir(path)
41
+ }
42
+
43
+ export function unlink(path: string): Result<void, FSError> {
44
+ return native.unlink(path)
45
+ }
46
+
47
+ export function rename(from: string, to: string): Result<void, FSError> {
48
+ return native.rename(from, to)
49
+ }
50
+
51
+ export function mkdir(path: string, options?: MkdirOptions): Result<void, FSError> {
52
+ return native.mkdir(path, options)
53
+ }
54
+
55
+ export function rmdir(path: string): Result<void, FSError> {
56
+ return native.rmdir(path)
57
+ }
58
+
59
+ export const exists = native.exists
60
+
61
+ export function readStream(
62
+ path: string,
63
+ options?: ReadStreamOptions,
64
+ ): Result<AsyncIterable<Uint8Array>, FSError> {
65
+ const openResult = native.open(path)
66
+ if (!openResult.ok) return openResult
67
+ const fh = openResult.value
68
+ const chunkSize = options?.chunkSize ?? 512
69
+
70
+ async function* iterate(): AsyncIterable<Uint8Array> {
71
+ try {
72
+ while (true) {
73
+ const r = fh.read(chunkSize)
74
+ if (!r.ok) {
75
+ // Handle-level errors (BadFileDescriptor, Unknown) don't carry a
76
+ // path from the native side. All other variants include `path`
77
+ // when emitted by path-level ops — but fh.read doesn't have the
78
+ // path, so we decorate here to preserve the original readStream
79
+ // context.
80
+ const e = r.error
81
+ if (e.name === 'BadFileDescriptor' || e.name === 'Unknown') throw e
82
+ throw {...e, path}
83
+ }
84
+ if (r.value === undefined) break
85
+ yield r.value
86
+ }
87
+ } finally {
88
+ fh.close()
89
+ }
90
+ }
91
+
92
+ return ok(iterate())
93
+ }
@@ -0,0 +1,92 @@
1
+ import type {Result} from '../result/types.js'
2
+
3
+ /**
4
+ * Curated FSError variants. Each carries only the context that's
5
+ * meaningful for that error:
6
+ * - `path` for most entries
7
+ * - `limit` for TooLarge (the readFile cap; currently always absent
8
+ * because native doesn't report the configured fs_read_max back)
9
+ * - `code`/`errno`/`message` for Unknown, carrying the raw native
10
+ * error through for debugging when a POSIX errno falls outside
11
+ * the curated set
12
+ *
13
+ * Discriminate on `.name`.
14
+ */
15
+ export type FSError =
16
+ | {name: 'NotFound'; path: string}
17
+ | {name: 'AlreadyExists'; path: string}
18
+ | {name: 'AccessDenied'; path: string}
19
+ | {name: 'NoSpace'; path: string}
20
+ | {name: 'TooLarge'; path: string; limit?: number}
21
+ | {name: 'IsDirectory'; path: string}
22
+ | {name: 'NotDirectory'; path: string}
23
+ | {name: 'BadFileDescriptor'}
24
+ | {name: 'Unknown'; code: number; errno: number | undefined; message: string}
25
+
26
+ export interface WriteFileOptions {
27
+ /** If false, fail with NotFound when the file doesn't exist. Default true. */
28
+ create?: boolean
29
+ /** If true, append instead of truncating. Default false. */
30
+ append?: boolean
31
+ }
32
+
33
+ export declare function writeFile(
34
+ path: string,
35
+ contents: string | Uint8Array,
36
+ options?: WriteFileOptions,
37
+ ): Result<void, FSError>
38
+
39
+ export declare function readFile(path: string): Result<Uint8Array, FSError>
40
+ export declare function readFile(path: string, encoding: 'utf-8'): Result<string, FSError>
41
+
42
+ export interface StatResult {
43
+ size: number
44
+ isDirectory: boolean
45
+ isFile: boolean
46
+ /** Modification time in ms since epoch. Absent if the platform doesn't
47
+ * track it (e.g. LittleFS without CONFIG_LITTLEFS_USE_MTIME). */
48
+ mtime?: number
49
+ }
50
+
51
+ export declare function stat(path: string): Result<StatResult, FSError>
52
+
53
+ export interface DirEntry {
54
+ name: string
55
+ isFile: boolean
56
+ isDirectory: boolean
57
+ }
58
+
59
+ export declare function readDir(path: string): Result<DirEntry[], FSError>
60
+ export declare function unlink(path: string): Result<void, FSError>
61
+ export declare function rename(from: string, to: string): Result<void, FSError>
62
+
63
+ export interface MkdirOptions {
64
+ /** Create intermediate directories as needed (like `mkdir -p`). Default false. */
65
+ recursive?: boolean
66
+ }
67
+
68
+ export declare function mkdir(path: string, options?: MkdirOptions): Result<void, FSError>
69
+ export declare function rmdir(path: string): Result<void, FSError>
70
+
71
+ /** Returns true when the path exists and is reachable, false otherwise.
72
+ * Never fails — resolution errors collapse to false. */
73
+ export declare function exists(path: string): boolean
74
+
75
+ export interface ReadStreamOptions {
76
+ /** Bytes to read per chunk. Default 512. */
77
+ chunkSize?: number
78
+ }
79
+
80
+ /**
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 propagate
83
+ * via rejected promises from `next()`.
84
+ *
85
+ * Compose with `mikrojs/stream` helpers (`decodeUtf8`, `splitLines`) for
86
+ * text/line streams. Closes the underlying handle on EOF, mid-stream
87
+ * error, or early consumer break.
88
+ */
89
+ export declare function readStream(
90
+ path: string,
91
+ options?: ReadStreamOptions,
92
+ ): Result<AsyncIterable<Uint8Array>, FSError>
@@ -0,0 +1,87 @@
1
+ // Global APIs available in the QuickJS runtime environment
2
+
3
+ declare function setTimeout(callback: (...args: any[]) => void, ms?: number): number
4
+ declare function clearTimeout(id: number): void
5
+
6
+ declare function setInterval(callback: (...args: any[]) => void, ms?: number): number
7
+ declare function clearInterval(id: number): void
8
+
9
+ declare const console: {
10
+ log(...args: unknown[]): void
11
+ warn(...args: unknown[]): void
12
+ error(...args: unknown[]): void
13
+ info(...args: unknown[]): void
14
+ debug(...args: unknown[]): void
15
+ }
16
+
17
+ declare function btoa(data: string): string
18
+ declare function atob(data: string): string
19
+
20
+ declare class TextEncoder {
21
+ encode(input?: string): Uint8Array
22
+ }
23
+
24
+ declare class TextDecoder {
25
+ constructor(label?: 'utf-8' | 'utf8')
26
+ decode(input?: ArrayBufferView | ArrayBuffer, options?: {stream?: boolean}): string
27
+ }
28
+
29
+ declare class AbortError extends Error {
30
+ constructor(message?: string)
31
+ readonly name: 'AbortError'
32
+ }
33
+
34
+ declare class TimeoutError extends Error {
35
+ constructor(message?: string)
36
+ readonly name: 'TimeoutError'
37
+ }
38
+
39
+ declare class AbortSignal {
40
+ readonly aborted: boolean
41
+ readonly reason: unknown
42
+ throwIfAborted(): void
43
+ onabort: (() => void) | null
44
+ addEventListener(type: 'abort', fn: () => void): void
45
+ removeEventListener(type: 'abort', fn: () => void): void
46
+ static abort(reason?: unknown): AbortSignal
47
+ static timeout(ms: number): AbortSignal
48
+ static any(signals: AbortSignal[]): AbortSignal
49
+ }
50
+
51
+ declare class AbortController {
52
+ readonly signal: AbortSignal
53
+ abort(reason?: unknown): void
54
+ }
55
+
56
+ declare interface ImportMeta {
57
+ readonly url: string
58
+ readonly main: boolean
59
+ readonly dirname: string
60
+ readonly basename: string
61
+ readonly path: string
62
+ readonly env: Readonly<Record<string, string | undefined>>
63
+ /**
64
+ * Evict a module from the runtime's module cache, recursively freeing
65
+ * any transitive dependency whose only importer was the unloaded module.
66
+ * Builtin modules (native:*, mikrojs/*, @mikrojs/*) are anchored
67
+ * and cannot be unloaded.
68
+ *
69
+ * Intended for use with dynamic imports: a static `import` at the top of
70
+ * the module creates a binding that pins the imported module's exports,
71
+ * so unloading it while the binding is live reclaims nothing. Prefer
72
+ * `const x = await import(spec)` + local scope + `import.meta.unload(spec)`.
73
+ *
74
+ * **Incompatible with `bundle: true`.** When the build bundles modules
75
+ * into chunks, source-level specifiers like `./phases/display.js` no
76
+ * longer correspond to loaded module names (they become chunk names like
77
+ * `display-GYVGL5MB.js`) — the unload call silently resolves to 0 and
78
+ * frees nothing. Projects that rely on dynamic-import-then-unload for
79
+ * memory reclaim need `bundle: false` so specifier strings survive to
80
+ * runtime unchanged.
81
+ *
82
+ * Resolves to the number of modules actually freed (root + transitive
83
+ * orphans), after a GC pass has run so a subsequent `memoryUsage()`
84
+ * snapshot reflects the reclaim.
85
+ */
86
+ unload(specifier: string): Promise<number>
87
+ }
@@ -0,0 +1,222 @@
1
+ // Transport-agnostic HTTP request/response primitives. No dependency on
2
+ // `native:http`. Apps driving HTTP through a non-lwIP transport (e.g. an
3
+ // LTE modem over UART) implement the `Request` type directly and reuse
4
+ // `prepareBody` + `makeResponse` for the boring parts.
5
+
6
+ import type {Result} from 'mikrojs/result'
7
+
8
+ export interface RequestOptions {
9
+ method?: string
10
+ /** Headers as pairs or a record. Keys used as-is (no implicit case change). */
11
+ headers?: [string, string][] | Record<string, string>
12
+ /** Shortcut: stringify as JSON and set `content-type: application/json`. */
13
+ json?: unknown
14
+ /** Raw body. Mutually exclusive with `json`. */
15
+ body?: Uint8Array | string
16
+ /** Total wallclock deadline; transport enforces. */
17
+ timeoutMs?: number
18
+ /** Transport owns listener install + cleanup. Maps abort to RequestError.Aborted. */
19
+ signal?: AbortSignal
20
+ }
21
+
22
+ export interface Response {
23
+ status: number
24
+ statusText: string
25
+ url: string
26
+ redirected: boolean
27
+ headers: [string, string][]
28
+ ok: boolean
29
+ /**
30
+ * Response body as an async iterable of chunks. Single-shot: after any of
31
+ * `body`, `text()`, `bytes()`, or `json()` has started draining, calling
32
+ * another consumer throws `BodyConsumedError`.
33
+ */
34
+ body: AsyncIterable<Uint8Array>
35
+ /** First matching header value, case-insensitive, or undefined if absent. */
36
+ get(name: string): string | undefined
37
+ /** All matching header values in order, case-insensitive. Empty array if absent. */
38
+ getAll(name: string): string[]
39
+ /** Drain body as a UTF-8 string. Throws `BodyConsumedError` on second consumer call. */
40
+ text(): Promise<string>
41
+ /** Drain body and parse as JSON. Throws `BodyConsumedError` on second consumer call. */
42
+ json(): Promise<unknown>
43
+ /** Drain body to a single `Uint8Array`. Throws `BodyConsumedError` on second consumer call. */
44
+ bytes(): Promise<Uint8Array>
45
+ /**
46
+ * Cancel the request and release any native resources. Safe to call after
47
+ * the body has been consumed (no-op). Safe to call multiple times.
48
+ */
49
+ close(): Promise<void>
50
+ }
51
+
52
+ export type Request = (
53
+ url: string,
54
+ options?: RequestOptions,
55
+ ) => Promise<Result<Response, RequestError>>
56
+
57
+ export const RequestError = {
58
+ Hardware: (message: string) => ({name: 'Hardware', message}) as const,
59
+ Network: (message: string) => ({name: 'Network', message}) as const,
60
+ Timeout: (message: string) => ({name: 'Timeout', message}) as const,
61
+ /** Request body exceeded the transport's per-request cap. */
62
+ BodyTooLarge: (size: number, cap: number) => ({name: 'BodyTooLarge', size, cap}) as const,
63
+ /** Transport returned bytes that could not be parsed as an HTTP response. */
64
+ InvalidResponse: (message: string) => ({name: 'InvalidResponse', message}) as const,
65
+ Aborted: (message: string) => ({name: 'Aborted', message}) as const,
66
+ /** Transport is at its concurrent-request cap. Retry after in-flight requests settle. */
67
+ TooManyPending: () => ({name: 'TooManyPending'}) as const,
68
+ }
69
+
70
+ export type RequestError =
71
+ | ReturnType<typeof RequestError.Hardware>
72
+ | ReturnType<typeof RequestError.Network>
73
+ | ReturnType<typeof RequestError.Timeout>
74
+ | ReturnType<typeof RequestError.BodyTooLarge>
75
+ | ReturnType<typeof RequestError.InvalidResponse>
76
+ | ReturnType<typeof RequestError.Aborted>
77
+ | ReturnType<typeof RequestError.TooManyPending>
78
+
79
+ export class BodyConsumedError extends Error {
80
+ readonly name = 'BodyConsumed'
81
+ constructor() {
82
+ super('response body already consumed')
83
+ }
84
+ }
85
+
86
+ const textEncoder = new TextEncoder()
87
+
88
+ /**
89
+ * Normalize `RequestOptions` into the byte-oriented shape a transport actually
90
+ * sends: final body bytes (or null) and header pairs. Applies the `json`
91
+ * shortcut (encodes + sets content-type unless already present) and
92
+ * UTF-8-encodes string bodies.
93
+ */
94
+ export function prepareBody(opts: RequestOptions): {
95
+ body: Uint8Array | null
96
+ headers: [string, string][]
97
+ } {
98
+ const headers = normalizeHeaders(opts.headers)
99
+ if (opts.json !== undefined) {
100
+ if (!hasHeader(headers, 'content-type')) {
101
+ headers.push(['content-type', 'application/json'])
102
+ }
103
+ return {body: textEncoder.encode(JSON.stringify(opts.json)), headers}
104
+ }
105
+ if (opts.body === undefined) return {body: null, headers}
106
+ if (typeof opts.body === 'string') return {body: textEncoder.encode(opts.body), headers}
107
+ return {body: opts.body, headers}
108
+ }
109
+
110
+ /**
111
+ * Wrap a transport's raw response (async-iterable body) into the public
112
+ * `Response` shape with `text()`/`json()`/`bytes()`/`get()`/`getAll()`/`close()`
113
+ * and a single-shot body claim.
114
+ */
115
+ export function makeResponse(raw: {
116
+ status: number
117
+ statusText: string
118
+ url: string
119
+ redirected: boolean
120
+ headers: [string, string][]
121
+ body: AsyncIterable<Uint8Array>
122
+ }): Response {
123
+ let consumed = false
124
+ const claim = () => {
125
+ if (consumed) throw new BodyConsumedError()
126
+ consumed = true
127
+ }
128
+
129
+ const body: AsyncIterable<Uint8Array> = {
130
+ [Symbol.asyncIterator]() {
131
+ claim()
132
+ return raw.body[Symbol.asyncIterator]()
133
+ },
134
+ }
135
+
136
+ return {
137
+ status: raw.status,
138
+ statusText: raw.statusText,
139
+ url: raw.url,
140
+ redirected: raw.redirected,
141
+ headers: raw.headers,
142
+ ok: raw.status >= 200 && raw.status < 300,
143
+ body,
144
+ get: (name) => {
145
+ const lower = name.toLowerCase()
146
+ for (const [k, v] of raw.headers) {
147
+ if (k.toLowerCase() === lower) return v
148
+ }
149
+ return undefined
150
+ },
151
+ getAll: (name) => {
152
+ const lower = name.toLowerCase()
153
+ const out: string[] = []
154
+ for (const [k, v] of raw.headers) {
155
+ if (k.toLowerCase() === lower) out.push(v)
156
+ }
157
+ return out
158
+ },
159
+ text: async () => {
160
+ claim()
161
+ return await drainAsText(raw.body)
162
+ },
163
+ json: async () => {
164
+ claim()
165
+ return JSON.parse(await drainAsText(raw.body))
166
+ },
167
+ bytes: async () => {
168
+ claim()
169
+ return await drain(raw.body)
170
+ },
171
+ close: async () => {
172
+ consumed = true
173
+ const it = raw.body[Symbol.asyncIterator]()
174
+ if (it.return) await it.return()
175
+ },
176
+ }
177
+ }
178
+
179
+ function normalizeHeaders(
180
+ input: [string, string][] | Record<string, string> | undefined,
181
+ ): [string, string][] {
182
+ if (!input) return []
183
+ if (Array.isArray(input)) return input.map(([k, v]) => [k, v])
184
+ const out: [string, string][] = []
185
+ for (const key of Object.keys(input)) out.push([key, input[key]!])
186
+ return out
187
+ }
188
+
189
+ function hasHeader(headers: [string, string][], name: string): boolean {
190
+ const lower = name.toLowerCase()
191
+ for (const [k] of headers) {
192
+ if (k.toLowerCase() === lower) return true
193
+ }
194
+ return false
195
+ }
196
+
197
+ async function drain(source: AsyncIterable<Uint8Array>): Promise<Uint8Array> {
198
+ const parts: Uint8Array[] = []
199
+ let total = 0
200
+ for await (const chunk of source) {
201
+ parts.push(chunk)
202
+ total += chunk.length
203
+ }
204
+ if (parts.length === 1) return parts[0]!
205
+ const out = new Uint8Array(total)
206
+ let offset = 0
207
+ for (const p of parts) {
208
+ out.set(p, offset)
209
+ offset += p.length
210
+ }
211
+ return out
212
+ }
213
+
214
+ async function drainAsText(source: AsyncIterable<Uint8Array>): Promise<string> {
215
+ const decoder = new TextDecoder()
216
+ let out = ''
217
+ for await (const chunk of source) {
218
+ out += decoder.decode(chunk, {stream: true})
219
+ }
220
+ out += decoder.decode()
221
+ return out
222
+ }
@@ -0,0 +1,151 @@
1
+ // Internal: native:http-backed request implementation. Bundled into
2
+ // http/request.js; not exposed as its own subpath. Exists as a seam so host
3
+ // tests can inject a fake native module.
4
+
5
+ import {
6
+ makeResponse,
7
+ prepareBody,
8
+ type Request,
9
+ RequestError,
10
+ type RequestOptions,
11
+ } from 'mikrojs/http/helpers'
12
+ import {err, ok} from 'mikrojs/result'
13
+
14
+ import type {ErrResult, Result} from '../result/types.js'
15
+
16
+ type HttpRequestStart =
17
+ | {
18
+ ok: true
19
+ id: number
20
+ headers: Promise<Result<{status: number; headers: [string, string][]}, RequestError>>
21
+ }
22
+ | ErrResult<RequestError>
23
+
24
+ export interface NativeHttpModule {
25
+ request: (
26
+ url: string,
27
+ options?: {method?: string; body?: Uint8Array; headers?: [string, string][]},
28
+ ) => HttpRequestStart
29
+ nextMessage: (
30
+ id: number,
31
+ ) => Promise<
32
+ | {kind: 'chunk'; data: Uint8Array}
33
+ | {kind: 'end'}
34
+ | {kind: 'error'; cancelled: boolean; message: string}
35
+ >
36
+ cancel: (id: number) => void
37
+ pendingCount: () => number
38
+ }
39
+
40
+ /**
41
+ * Build a `Request` function driven by a `native:http`-shaped module. Owns
42
+ * AbortSignal listener lifecycle and total-wallclock timeout enforcement.
43
+ *
44
+ * Callers are responsible for releasing the native pending slot by either
45
+ * draining the response body or calling `response.close()`. Leaks are
46
+ * observable via `pendingCount()`.
47
+ */
48
+ export function createRequestFromNative(native: NativeHttpModule): Request {
49
+ return async (url: string, options: RequestOptions = {}) => {
50
+ if (options.signal?.aborted) {
51
+ return err(RequestError.Aborted(String(options.signal.reason ?? 'aborted')))
52
+ }
53
+
54
+ const {body, headers} = prepareBody(options)
55
+ const method = (options.method ?? 'GET').toUpperCase()
56
+
57
+ const start = native.request(url, {
58
+ method,
59
+ body: body ?? undefined,
60
+ headers,
61
+ })
62
+ if (!start.ok) return start
63
+
64
+ const id = start.id
65
+
66
+ let cleaned = false
67
+ let timeoutId: ReturnType<typeof setTimeout> | undefined
68
+ let abortHandler: (() => void) | undefined
69
+ const cleanup = () => {
70
+ if (cleaned) return
71
+ cleaned = true
72
+ if (abortHandler && options.signal) {
73
+ options.signal.removeEventListener('abort', abortHandler)
74
+ }
75
+ if (timeoutId !== undefined) clearTimeout(timeoutId)
76
+ }
77
+
78
+ if (options.timeoutMs !== undefined) {
79
+ timeoutId = setTimeout(() => native.cancel(id), options.timeoutMs)
80
+ }
81
+ if (options.signal) {
82
+ abortHandler = () => native.cancel(id)
83
+ options.signal.addEventListener('abort', abortHandler)
84
+ }
85
+
86
+ const headersResult = await start.headers
87
+ if (!headersResult.ok) {
88
+ cleanup()
89
+ return headersResult
90
+ }
91
+
92
+ const {status, headers: responseHeaders} = headersResult.value
93
+
94
+ let done = false
95
+ const rawBody: AsyncIterable<Uint8Array> = {
96
+ [Symbol.asyncIterator]() {
97
+ return {
98
+ async next() {
99
+ if (done) return {done: true, value: undefined}
100
+ let msg
101
+ try {
102
+ msg = await native.nextMessage(id)
103
+ } catch (e) {
104
+ done = true
105
+ cleanup()
106
+ throw e
107
+ }
108
+ if (msg.kind === 'chunk') {
109
+ return {done: false, value: msg.data}
110
+ }
111
+ done = true
112
+ cleanup()
113
+ if (msg.kind === 'end') return {done: true, value: undefined}
114
+ /* error */
115
+ if (msg.cancelled) {
116
+ throw RequestError.Aborted(msg.message || String(options.signal?.reason ?? 'aborted'))
117
+ }
118
+ throw RequestError.Network(msg.message || 'HTTP request failed')
119
+ },
120
+ async return() {
121
+ if (!done) {
122
+ done = true
123
+ cleanup()
124
+ native.cancel(id)
125
+ try {
126
+ for (;;) {
127
+ const msg = await native.nextMessage(id)
128
+ if (msg.kind !== 'chunk') break
129
+ }
130
+ } catch {
131
+ /* ignore */
132
+ }
133
+ }
134
+ return {done: true, value: undefined}
135
+ },
136
+ }
137
+ },
138
+ }
139
+
140
+ return ok(
141
+ makeResponse({
142
+ status,
143
+ statusText: '',
144
+ url,
145
+ redirected: false,
146
+ headers: responseHeaders,
147
+ body: rawBody,
148
+ }),
149
+ )
150
+ }
151
+ }
@@ -0,0 +1,25 @@
1
+ import * as native from 'native:http'
2
+
3
+ import {createRequestFromNative} from './native.js'
4
+
5
+ /**
6
+ * Default HTTP request for the mikrojs runtime. Uses the ESP32 `native:http`
7
+ * transport, which streams response bodies chunk-by-chunk with reader-driven
8
+ * backpressure, integrates with `AbortSignal`, and enforces total-wallclock
9
+ * timeouts.
10
+ *
11
+ * For `RequestError`, `BodyConsumedError`, `prepareBody`, `makeResponse`, or
12
+ * any of the shared types, import from `mikrojs/http/helpers`. That subpath
13
+ * has no dependency on `native:http`, so importing it from custom transports
14
+ * (e.g. an LTE modem) doesn't retain the WiFi-backed HTTP stack.
15
+ */
16
+ export const request = createRequestFromNative(native)
17
+
18
+ /**
19
+ * Number of in-flight requests whose terminal message has not yet been
20
+ * consumed. Primarily useful in tests and diagnostic code to verify that
21
+ * cancel + drain correctly releases request slots.
22
+ */
23
+ export function pendingCount(): number {
24
+ return native.pendingCount()
25
+ }