@mikrojs/firmware 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.
- package/LICENSE +21 -0
- package/README.md +30 -0
- package/bin/idf.py +7 -0
- package/chips.json +3 -0
- package/cmake.js +9 -0
- package/components/mikrojs/CMakeLists.txt +187 -0
- package/components/mikrojs/Kconfig +55 -0
- package/components/mikrojs/idf_component.yml +6 -0
- package/components/mikrojs/include/mem.h +3 -0
- package/components/mikrojs/include/mik_color.h +3 -0
- package/components/mikrojs/include/mik_http_internal.h +77 -0
- package/components/mikrojs/include/mikrojs.h +5 -0
- package/components/mikrojs/include/mikrojs_esp32.h +65 -0
- package/components/mikrojs/include/private.h +10 -0
- package/components/mikrojs/include/utils.h +3 -0
- package/components/mikrojs/mik_ble.cpp +1588 -0
- package/components/mikrojs/mik_ble_c_shim.c +61 -0
- package/components/mikrojs/mik_ble_c_shim.h +37 -0
- package/components/mikrojs/mik_config.cpp +167 -0
- package/components/mikrojs/mik_deploy.cpp +584 -0
- package/components/mikrojs/mik_http.cpp +916 -0
- package/components/mikrojs/mik_i2c.cpp +364 -0
- package/components/mikrojs/mik_main.cpp +542 -0
- package/components/mikrojs/mik_neopixel.cpp +437 -0
- package/components/mikrojs/mik_nvs_kv.cpp +219 -0
- package/components/mikrojs/mik_pin.cpp +195 -0
- package/components/mikrojs/mik_pwm.cpp +525 -0
- package/components/mikrojs/mik_recovery.cpp +86 -0
- package/components/mikrojs/mik_rtc.cpp +305 -0
- package/components/mikrojs/mik_serial_io.cpp +362 -0
- package/components/mikrojs/mik_sleep.cpp +226 -0
- package/components/mikrojs/mik_sntp.cpp +275 -0
- package/components/mikrojs/mik_spi.cpp +330 -0
- package/components/mikrojs/mik_uart.cpp +497 -0
- package/components/mikrojs/mik_wifi.cpp +1434 -0
- package/components/mikrojs/platform_esp32.cpp +192 -0
- package/components/mikrojs/test/CMakeLists.txt +32 -0
- package/components/mikrojs/test/abort_test.cpp +254 -0
- package/components/mikrojs/test/ble_test.cpp +714 -0
- package/components/mikrojs/test/fs_js_test.cpp +458 -0
- package/components/mikrojs/test/fs_pub_test.cpp +312 -0
- package/components/mikrojs/test/http_test.cpp +475 -0
- package/components/mikrojs/test/i2c_test.cpp +138 -0
- package/components/mikrojs/test/modules_extended_test.cpp +137 -0
- package/components/mikrojs/test/modules_test.cpp +131 -0
- package/components/mikrojs/test/pins_test.cpp +47 -0
- package/components/mikrojs/test/pwm_test.cpp +166 -0
- package/components/mikrojs/test/repl_protocol_test.cpp +405 -0
- package/components/mikrojs/test/rtc_test.cpp +331 -0
- package/components/mikrojs/test/runtime_test.cpp +89 -0
- package/components/mikrojs/test/sleep_test.cpp +222 -0
- package/components/mikrojs/test/sntp_test.cpp +249 -0
- package/components/mikrojs/test/stdio_test.cpp +449 -0
- package/components/mikrojs/test/sys_test.cpp +165 -0
- package/components/mikrojs/test/text_encoding_test.cpp +224 -0
- package/components/mikrojs/test/timers_js_test.cpp +244 -0
- package/components/mikrojs/test/timers_test.cpp +79 -0
- package/components/mikrojs/test/wifi_test.cpp +599 -0
- package/default-app/main/CMakeLists.txt +3 -0
- package/default-app/main/main.cpp +5 -0
- package/discover.js +77 -0
- package/index.d.ts +7 -0
- package/index.js +20 -0
- package/package.json +61 -0
- package/partitions.csv +5 -0
- package/prebuilds/esp32/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32/flasher_args.json +24 -0
- package/prebuilds/esp32/mikrojs.bin +0 -0
- package/prebuilds/esp32/partition_table/partition-table.bin +0 -0
- package/prebuilds/esp32c3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c3/flasher_args.json +24 -0
- package/prebuilds/esp32c3/mikrojs.bin +0 -0
- package/prebuilds/esp32c3/partition_table/partition-table.bin +0 -0
- package/prebuilds/esp32c6/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c6/flasher_args.json +24 -0
- package/prebuilds/esp32c6/mikrojs.bin +0 -0
- package/prebuilds/esp32c6/partition_table/partition-table.bin +0 -0
- package/prebuilds/esp32s3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32s3/flasher_args.json +24 -0
- package/prebuilds/esp32s3/mikrojs.bin +0 -0
- package/prebuilds/esp32s3/partition_table/partition-table.bin +0 -0
- package/project.cmake +101 -0
- package/resolve.js +54 -0
- package/sdkconfig.defaults +127 -0
- package/sdkconfig.defaults.esp32 +8 -0
- package/sdkconfig.defaults.esp32c3 +15 -0
- package/sdkconfig.defaults.esp32c6 +26 -0
- package/sdkconfig.defaults.esp32s3 +22 -0
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
#include <atomic>
|
|
2
|
+
#include <cstring>
|
|
3
|
+
|
|
4
|
+
#include "esp_crt_bundle.h"
|
|
5
|
+
#include "esp_event.h"
|
|
6
|
+
#include "esp_http_client.h"
|
|
7
|
+
#include "esp_log.h"
|
|
8
|
+
#include "esp_netif.h"
|
|
9
|
+
#include "freertos/FreeRTOS.h"
|
|
10
|
+
#include "freertos/queue.h"
|
|
11
|
+
#include "freertos/semphr.h"
|
|
12
|
+
#include "freertos/task.h"
|
|
13
|
+
#include "mik_http_internal.h"
|
|
14
|
+
#include "private.h"
|
|
15
|
+
#include "utils.h"
|
|
16
|
+
|
|
17
|
+
/* Dynamic module data slot, allocated on first import.
|
|
18
|
+
* Non-static so tests can access it via extern. */
|
|
19
|
+
int mik__http_slot = -1;
|
|
20
|
+
|
|
21
|
+
/* Helper to access HTTP state from runtime module_data slot */
|
|
22
|
+
static inline MIKHttpState*& mik__http_st(MIKRuntime* rt) {
|
|
23
|
+
return reinterpret_cast<MIKHttpState*&>(rt->module_data[mik__http_slot]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#define MIK_HTTP_TAG "native:http"
|
|
27
|
+
/* 8 KB was tight for HTTPS — mbedTLS handshake + request header buffer can
|
|
28
|
+
* spike the task stack; the ESP-HTTP-client header list was corrupted once
|
|
29
|
+
* the stack overran. 12 KB matches the recommendation for esp-tls users. */
|
|
30
|
+
#define MIK_HTTP_TASK_STACK_SIZE 12288
|
|
31
|
+
#define MIK_HTTP_CHUNK_SIZE 2048
|
|
32
|
+
/* MIK_HTTP_MAX_PENDING and MIK_HTTP_MAX_CHUNKS_INFLIGHT live in
|
|
33
|
+
* mik_http_internal.h so tests can reference the same ceilings. */
|
|
34
|
+
|
|
35
|
+
/* ── Types ─────────────────────────────────────────────────────────── */
|
|
36
|
+
|
|
37
|
+
struct MIKHttpRequest {
|
|
38
|
+
char* url;
|
|
39
|
+
esp_http_client_method_t method;
|
|
40
|
+
uint8_t* body;
|
|
41
|
+
size_t body_len;
|
|
42
|
+
MIKHttpHeader* headers;
|
|
43
|
+
size_t header_count;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/* ── Background task ───────────────────────────────────────────────── */
|
|
47
|
+
|
|
48
|
+
struct MIKHttpTaskArgs {
|
|
49
|
+
uint32_t id;
|
|
50
|
+
MIKHttpRequest req;
|
|
51
|
+
QueueHandle_t result_queue;
|
|
52
|
+
SemaphoreHandle_t inflight;
|
|
53
|
+
std::atomic<bool>* cancelled;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
static esp_http_client_method_t mik__method_from_string(const char* method) {
|
|
57
|
+
if (!method) return HTTP_METHOD_GET;
|
|
58
|
+
if (strcasecmp(method, "GET") == 0) return HTTP_METHOD_GET;
|
|
59
|
+
if (strcasecmp(method, "POST") == 0) return HTTP_METHOD_POST;
|
|
60
|
+
if (strcasecmp(method, "PUT") == 0) return HTTP_METHOD_PUT;
|
|
61
|
+
if (strcasecmp(method, "PATCH") == 0) return HTTP_METHOD_PATCH;
|
|
62
|
+
if (strcasecmp(method, "DELETE") == 0) return HTTP_METHOD_DELETE;
|
|
63
|
+
if (strcasecmp(method, "HEAD") == 0) return HTTP_METHOD_HEAD;
|
|
64
|
+
if (strcasecmp(method, "OPTIONS") == 0) return HTTP_METHOD_OPTIONS;
|
|
65
|
+
return HTTP_METHOD_GET;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static void mik__http_free_headers(MIKHttpHeader* headers, size_t count) {
|
|
69
|
+
for (size_t i = 0; i < count; i++) {
|
|
70
|
+
free(headers[i].key);
|
|
71
|
+
free(headers[i].value);
|
|
72
|
+
}
|
|
73
|
+
free(headers);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static void mik__http_free_request(MIKHttpRequest* req) {
|
|
77
|
+
free(req->url);
|
|
78
|
+
free(req->body);
|
|
79
|
+
mik__http_free_headers(req->headers, req->header_count);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
struct MIKHttpEventData {
|
|
83
|
+
MIKHttpHeader* headers;
|
|
84
|
+
size_t header_count;
|
|
85
|
+
size_t header_capacity;
|
|
86
|
+
bool oom;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
static esp_err_t mik__http_event_handler(esp_http_client_event_t* evt) {
|
|
90
|
+
auto* data = static_cast<MIKHttpEventData*>(evt->user_data);
|
|
91
|
+
if (evt->event_id == HTTP_EVENT_ON_HEADER && data && !data->oom) {
|
|
92
|
+
if (data->header_count >= data->header_capacity) {
|
|
93
|
+
size_t new_cap = data->header_capacity == 0 ? 8 : data->header_capacity * 2;
|
|
94
|
+
auto* new_headers = static_cast<MIKHttpHeader*>(
|
|
95
|
+
realloc(data->headers, new_cap * sizeof(MIKHttpHeader)));
|
|
96
|
+
if (!new_headers) {
|
|
97
|
+
data->oom = true;
|
|
98
|
+
return ESP_OK;
|
|
99
|
+
}
|
|
100
|
+
data->headers = new_headers;
|
|
101
|
+
data->header_capacity = new_cap;
|
|
102
|
+
}
|
|
103
|
+
char* key = strdup(evt->header_key);
|
|
104
|
+
char* value = strdup(evt->header_value);
|
|
105
|
+
if (!key || !value) {
|
|
106
|
+
free(key);
|
|
107
|
+
free(value);
|
|
108
|
+
data->oom = true;
|
|
109
|
+
return ESP_OK;
|
|
110
|
+
}
|
|
111
|
+
data->headers[data->header_count].key = key;
|
|
112
|
+
data->headers[data->header_count].value = value;
|
|
113
|
+
data->header_count++;
|
|
114
|
+
}
|
|
115
|
+
return ESP_OK;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static void mik__post_headers(QueueHandle_t q, uint32_t id, int status,
|
|
119
|
+
MIKHttpHeader* headers, size_t header_count) {
|
|
120
|
+
MIKHttpMsg m = {};
|
|
121
|
+
m.id = id;
|
|
122
|
+
m.kind = MIK_HTTP_MSG_HEADERS;
|
|
123
|
+
m.status = status;
|
|
124
|
+
m.headers = headers;
|
|
125
|
+
m.header_count = header_count;
|
|
126
|
+
xQueueSend(q, &m, portMAX_DELAY);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static void mik__post_chunk(QueueHandle_t q, uint32_t id, uint8_t* data, size_t len) {
|
|
130
|
+
MIKHttpMsg m = {};
|
|
131
|
+
m.id = id;
|
|
132
|
+
m.kind = MIK_HTTP_MSG_CHUNK;
|
|
133
|
+
m.chunk_data = data;
|
|
134
|
+
m.chunk_len = len;
|
|
135
|
+
xQueueSend(q, &m, portMAX_DELAY);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static void mik__post_end(QueueHandle_t q, uint32_t id) {
|
|
139
|
+
MIKHttpMsg m = {};
|
|
140
|
+
m.id = id;
|
|
141
|
+
m.kind = MIK_HTTP_MSG_END;
|
|
142
|
+
xQueueSend(q, &m, portMAX_DELAY);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static void mik__post_error(QueueHandle_t q, uint32_t id, const char* msg, bool cancelled) {
|
|
146
|
+
MIKHttpMsg m = {};
|
|
147
|
+
m.id = id;
|
|
148
|
+
m.kind = MIK_HTTP_MSG_ERROR;
|
|
149
|
+
m.is_cancelled = cancelled;
|
|
150
|
+
m.error_message = strdup(msg);
|
|
151
|
+
/* If strdup fails under OOM, the downstream consumer treats NULL as "" but
|
|
152
|
+
* the JS side then sees an empty error message. Leave NULL here — the
|
|
153
|
+
* empty-string fallback in mik__error_msg_value keeps the flow intact. */
|
|
154
|
+
xQueueSend(q, &m, portMAX_DELAY);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static bool mik__task_cancelled(MIKHttpTaskArgs* args) {
|
|
158
|
+
return args->cancelled->load(std::memory_order_relaxed);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Heap headroom note: sequential HTTPS requests on a 260KB heap produce
|
|
162
|
+
* fragmentation that is NOT a leak in this code — it's mbedTLS scatter-alloc
|
|
163
|
+
* during handshake plus lwIP holding socket TCBs in TIME_WAIT for 2*MSL
|
|
164
|
+
* (CONFIG_LWIP_TCP_MSL). Both reclaim over time; sysFree recovers after the
|
|
165
|
+
* TIME_WAIT window. The UAF fix in commit [UAF fix] removed the real bug;
|
|
166
|
+
* remaining drift is expected platform behavior, not worth chasing without
|
|
167
|
+
* a broader evaluation of CONFIG_MBEDTLS_DYNAMIC_BUFFER, session tickets,
|
|
168
|
+
* or moving to a custom esp-tls-direct HTTP implementation. */
|
|
169
|
+
static void mik__http_task(void* arg) {
|
|
170
|
+
auto* args = static_cast<MIKHttpTaskArgs*>(arg);
|
|
171
|
+
|
|
172
|
+
MIKHttpEventData event_data = {};
|
|
173
|
+
bool headers_posted = false;
|
|
174
|
+
bool cancelled = false;
|
|
175
|
+
char error_buf[256] = {0};
|
|
176
|
+
bool have_error = false;
|
|
177
|
+
|
|
178
|
+
esp_http_client_config_t config = {};
|
|
179
|
+
config.url = args->req.url;
|
|
180
|
+
config.method = args->req.method;
|
|
181
|
+
// 10s socket-level timeout. Lower than the default 30s so that a
|
|
182
|
+
// blocking read can't hold the BG task hostage past any reasonable
|
|
183
|
+
// JS-level timeoutMs (our cancel polling can't interrupt a blocking
|
|
184
|
+
// read — it only checks between reads).
|
|
185
|
+
config.timeout_ms = 10000;
|
|
186
|
+
config.disable_auto_redirect = false;
|
|
187
|
+
config.max_redirection_count = 10;
|
|
188
|
+
config.event_handler = mik__http_event_handler;
|
|
189
|
+
config.user_data = &event_data;
|
|
190
|
+
config.crt_bundle_attach = esp_crt_bundle_attach;
|
|
191
|
+
|
|
192
|
+
esp_http_client_handle_t client = esp_http_client_init(&config);
|
|
193
|
+
if (!client) {
|
|
194
|
+
snprintf(error_buf, sizeof(error_buf), "failed to init HTTP client");
|
|
195
|
+
have_error = true;
|
|
196
|
+
goto done;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (mik__task_cancelled(args)) {
|
|
200
|
+
cancelled = true;
|
|
201
|
+
goto cleanup;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (size_t i = 0; i < args->req.header_count; i++) {
|
|
205
|
+
esp_http_client_set_header(client, args->req.headers[i].key, args->req.headers[i].value);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (args->req.body && args->req.body_len > 0) {
|
|
209
|
+
esp_http_client_set_post_field(client, reinterpret_cast<const char*>(args->req.body),
|
|
210
|
+
args->req.body_len);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
{
|
|
214
|
+
esp_err_t err = esp_http_client_open(client, args->req.body_len);
|
|
215
|
+
if (err != ESP_OK) {
|
|
216
|
+
snprintf(error_buf, sizeof(error_buf), "fetch failed: could not connect to %s",
|
|
217
|
+
args->req.url);
|
|
218
|
+
have_error = true;
|
|
219
|
+
goto cleanup;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (mik__task_cancelled(args)) {
|
|
223
|
+
cancelled = true;
|
|
224
|
+
goto cleanup;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (args->req.body && args->req.body_len > 0) {
|
|
228
|
+
int wlen = esp_http_client_write(client, reinterpret_cast<const char*>(args->req.body),
|
|
229
|
+
args->req.body_len);
|
|
230
|
+
if (wlen < 0) {
|
|
231
|
+
snprintf(error_buf, sizeof(error_buf),
|
|
232
|
+
"fetch failed: could not send request body to %s", args->req.url);
|
|
233
|
+
have_error = true;
|
|
234
|
+
goto cleanup;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (mik__task_cancelled(args)) {
|
|
239
|
+
cancelled = true;
|
|
240
|
+
goto cleanup;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
int content_length = esp_http_client_fetch_headers(client);
|
|
244
|
+
if (content_length < 0 && esp_http_client_get_status_code(client) == 0) {
|
|
245
|
+
snprintf(error_buf, sizeof(error_buf), "fetch failed: error reading response from %s",
|
|
246
|
+
args->req.url);
|
|
247
|
+
have_error = true;
|
|
248
|
+
goto cleanup;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* Fail the request if the event handler hit OOM while collecting
|
|
252
|
+
* response headers — silently dropping headers would let missing
|
|
253
|
+
* Content-Type / Location / Set-Cookie cause downstream bugs. */
|
|
254
|
+
if (event_data.oom) {
|
|
255
|
+
snprintf(error_buf, sizeof(error_buf),
|
|
256
|
+
"fetch failed: out of memory collecting response headers");
|
|
257
|
+
have_error = true;
|
|
258
|
+
goto cleanup;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (mik__task_cancelled(args)) {
|
|
262
|
+
cancelled = true;
|
|
263
|
+
goto cleanup;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* Post HEADERS now that we have status + collected headers.
|
|
267
|
+
* Transfer ownership of event_data.headers into the message. */
|
|
268
|
+
int status = esp_http_client_get_status_code(client);
|
|
269
|
+
mik__post_headers(args->result_queue, args->id, status, event_data.headers,
|
|
270
|
+
event_data.header_count);
|
|
271
|
+
event_data.headers = nullptr;
|
|
272
|
+
event_data.header_count = 0;
|
|
273
|
+
headers_posted = true;
|
|
274
|
+
|
|
275
|
+
/* Stream body chunks. Each chunk takes one semaphore slot. */
|
|
276
|
+
while (true) {
|
|
277
|
+
if (mik__task_cancelled(args)) {
|
|
278
|
+
cancelled = true;
|
|
279
|
+
goto cleanup;
|
|
280
|
+
}
|
|
281
|
+
/* Wait for an inflight slot. Periodically wake to re-check cancel. */
|
|
282
|
+
if (xSemaphoreTake(args->inflight, pdMS_TO_TICKS(500)) != pdTRUE) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
auto* buf = static_cast<uint8_t*>(malloc(MIK_HTTP_CHUNK_SIZE));
|
|
287
|
+
if (!buf) {
|
|
288
|
+
xSemaphoreGive(args->inflight);
|
|
289
|
+
snprintf(error_buf, sizeof(error_buf), "out of memory");
|
|
290
|
+
have_error = true;
|
|
291
|
+
goto cleanup;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
int rlen = esp_http_client_read(client, reinterpret_cast<char*>(buf),
|
|
295
|
+
MIK_HTTP_CHUNK_SIZE);
|
|
296
|
+
if (rlen < 0) {
|
|
297
|
+
xSemaphoreGive(args->inflight);
|
|
298
|
+
free(buf);
|
|
299
|
+
snprintf(error_buf, sizeof(error_buf), "fetch failed: error reading body from %s",
|
|
300
|
+
args->req.url);
|
|
301
|
+
have_error = true;
|
|
302
|
+
goto cleanup;
|
|
303
|
+
}
|
|
304
|
+
if (rlen == 0) {
|
|
305
|
+
/* EOF. Slot still held — give it back; we don't need it. */
|
|
306
|
+
xSemaphoreGive(args->inflight);
|
|
307
|
+
free(buf);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
mik__post_chunk(args->result_queue, args->id, buf, rlen);
|
|
311
|
+
/* Slot ownership transfers to consume, which gives it back on dequeue. */
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
cleanup:
|
|
316
|
+
esp_http_client_close(client);
|
|
317
|
+
esp_http_client_cleanup(client);
|
|
318
|
+
|
|
319
|
+
done:
|
|
320
|
+
if (cancelled) {
|
|
321
|
+
if (!headers_posted) {
|
|
322
|
+
mik__http_free_headers(event_data.headers, event_data.header_count);
|
|
323
|
+
}
|
|
324
|
+
mik__post_error(args->result_queue, args->id, "cancelled", true);
|
|
325
|
+
} else if (have_error) {
|
|
326
|
+
if (!headers_posted) {
|
|
327
|
+
mik__http_free_headers(event_data.headers, event_data.header_count);
|
|
328
|
+
}
|
|
329
|
+
mik__post_error(args->result_queue, args->id, error_buf, false);
|
|
330
|
+
} else {
|
|
331
|
+
mik__post_end(args->result_queue, args->id);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
mik__http_free_request(&args->req);
|
|
335
|
+
/* Ownership of the `cancelled` atomic transfers to the JS side once the
|
|
336
|
+
* terminal message is posted. The BG task no longer reads it after this
|
|
337
|
+
* point, so it's safe for JS (consume / pending_drop / destroy) to free
|
|
338
|
+
* it whenever it sees fit — and JS holding the pointer via
|
|
339
|
+
* pending[].cancelled guarantees there's no UAF window when JS calls
|
|
340
|
+
* cancel() between our xQueueSend and vTaskDelete. */
|
|
341
|
+
free(args);
|
|
342
|
+
vTaskDelete(nullptr);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* ── JS bindings ──────────────────────────────────────────────────── */
|
|
346
|
+
|
|
347
|
+
static MIKHttpPending* mik__find_pending(MIKHttpState* state, uint32_t id) {
|
|
348
|
+
for (size_t i = 0; i < state->pending_count; i++) {
|
|
349
|
+
if (state->pending[i].id == id) return &state->pending[i];
|
|
350
|
+
}
|
|
351
|
+
return nullptr;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
static void mik__free_queued_msg(MIKHttpQueuedMsg* m) {
|
|
355
|
+
if (!m) return;
|
|
356
|
+
free(m->chunk_data);
|
|
357
|
+
free(m->error_message);
|
|
358
|
+
free(m);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* Callers must resolve headers_promise and next_promise (if active) before
|
|
362
|
+
* dropping the pending. This function does not have access to a JSContext
|
|
363
|
+
* to free them defensively. */
|
|
364
|
+
static void mik__pending_drop(MIKHttpState* state, size_t idx) {
|
|
365
|
+
MIKHttpPending* p = &state->pending[idx];
|
|
366
|
+
delete p->cancelled;
|
|
367
|
+
p->cancelled = nullptr;
|
|
368
|
+
MIKHttpQueuedMsg* m = p->queue_head;
|
|
369
|
+
while (m) {
|
|
370
|
+
MIKHttpQueuedMsg* next = m->next;
|
|
371
|
+
if (m->kind == MIK_HTTP_MSG_CHUNK) xSemaphoreGive(state->inflight);
|
|
372
|
+
mik__free_queued_msg(m);
|
|
373
|
+
m = next;
|
|
374
|
+
}
|
|
375
|
+
p->queue_head = nullptr;
|
|
376
|
+
p->queue_tail = nullptr;
|
|
377
|
+
state->pending[idx] = state->pending[--state->pending_count];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* Build a JS value representing a message for the JS side: either the headers
|
|
381
|
+
* result or the nextMessage result (both kinds). */
|
|
382
|
+
static JSValue mik__headers_result_ok(JSContext* ctx, int status, MIKHttpHeader* headers,
|
|
383
|
+
size_t header_count) {
|
|
384
|
+
JSValue obj = JS_NewObject(ctx);
|
|
385
|
+
JS_SetPropertyStr(ctx, obj, "status", JS_NewInt32(ctx, status));
|
|
386
|
+
JSValue js_headers = JS_NewArray(ctx);
|
|
387
|
+
for (size_t h = 0; h < header_count; h++) {
|
|
388
|
+
JSValue pair = JS_NewArray(ctx);
|
|
389
|
+
JS_SetPropertyUint32(ctx, pair, 0, JS_NewString(ctx, headers[h].key));
|
|
390
|
+
JS_SetPropertyUint32(ctx, pair, 1, JS_NewString(ctx, headers[h].value));
|
|
391
|
+
JS_SetPropertyUint32(ctx, js_headers, h, pair);
|
|
392
|
+
}
|
|
393
|
+
JS_SetPropertyStr(ctx, obj, "headers", js_headers);
|
|
394
|
+
return mik__result_ok(ctx, obj);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
static JSValue mik__chunk_msg_value(JSContext* ctx, uint8_t* data, size_t len) {
|
|
398
|
+
JSValue obj = JS_NewObject(ctx);
|
|
399
|
+
JS_SetPropertyStr(ctx, obj, "kind", JS_NewString(ctx, "chunk"));
|
|
400
|
+
JS_SetPropertyStr(ctx, obj, "data",
|
|
401
|
+
len > 0 ? JS_NewUint8ArrayCopy(ctx, data, len)
|
|
402
|
+
: JS_NewUint8ArrayCopy(ctx, nullptr, 0));
|
|
403
|
+
return obj;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
static JSValue mik__end_msg_value(JSContext* ctx) {
|
|
407
|
+
JSValue obj = JS_NewObject(ctx);
|
|
408
|
+
JS_SetPropertyStr(ctx, obj, "kind", JS_NewString(ctx, "end"));
|
|
409
|
+
return obj;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
static JSValue mik__error_msg_value(JSContext* ctx, bool cancelled, const char* message) {
|
|
413
|
+
JSValue obj = JS_NewObject(ctx);
|
|
414
|
+
JS_SetPropertyStr(ctx, obj, "kind", JS_NewString(ctx, "error"));
|
|
415
|
+
JS_SetPropertyStr(ctx, obj, "cancelled", JS_NewBool(ctx, cancelled));
|
|
416
|
+
JS_SetPropertyStr(ctx, obj, "message",
|
|
417
|
+
message ? JS_NewString(ctx, message) : JS_NewString(ctx, ""));
|
|
418
|
+
return obj;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* Resolve a pending's next_promise with a message. Caller sets *finished=true
|
|
422
|
+
* when END or ERROR was delivered so the caller can drop the pending entry. */
|
|
423
|
+
static void mik__resolve_next(JSContext* ctx, MIKHttpPending* p, MIKHttpMsgKind kind,
|
|
424
|
+
uint8_t* chunk_data, size_t chunk_len, bool is_cancelled,
|
|
425
|
+
const char* error_message, bool* finished) {
|
|
426
|
+
JSValue v;
|
|
427
|
+
*finished = false;
|
|
428
|
+
if (kind == MIK_HTTP_MSG_CHUNK) {
|
|
429
|
+
v = mik__chunk_msg_value(ctx, chunk_data, chunk_len);
|
|
430
|
+
} else if (kind == MIK_HTTP_MSG_END) {
|
|
431
|
+
v = mik__end_msg_value(ctx);
|
|
432
|
+
*finished = true;
|
|
433
|
+
} else {
|
|
434
|
+
v = mik__error_msg_value(ctx, is_cancelled, error_message);
|
|
435
|
+
*finished = true;
|
|
436
|
+
}
|
|
437
|
+
MIK_ResolvePromise(ctx, &p->next_promise, 1, &v);
|
|
438
|
+
p->next_promise_active = false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* Returns false on OOM. Callers must free the transferred payload themselves
|
|
442
|
+
* and release any associated resources (inflight semaphore for CHUNK). */
|
|
443
|
+
static bool mik__enqueue_msg(MIKHttpPending* p, const MIKHttpMsg& m) {
|
|
444
|
+
auto* q = static_cast<MIKHttpQueuedMsg*>(calloc(1, sizeof(MIKHttpQueuedMsg)));
|
|
445
|
+
if (!q) return false;
|
|
446
|
+
q->kind = m.kind;
|
|
447
|
+
if (m.kind == MIK_HTTP_MSG_CHUNK) {
|
|
448
|
+
q->chunk_data = m.chunk_data;
|
|
449
|
+
q->chunk_len = m.chunk_len;
|
|
450
|
+
} else if (m.kind == MIK_HTTP_MSG_ERROR) {
|
|
451
|
+
q->is_cancelled = m.is_cancelled;
|
|
452
|
+
q->error_message = m.error_message; // ownership transferred
|
|
453
|
+
}
|
|
454
|
+
if (p->queue_tail) {
|
|
455
|
+
p->queue_tail->next = q;
|
|
456
|
+
p->queue_tail = q;
|
|
457
|
+
} else {
|
|
458
|
+
p->queue_head = p->queue_tail = q;
|
|
459
|
+
}
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
static JSValue mik__http_request(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
464
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
465
|
+
CHECK_NOT_NULL(mik_rt);
|
|
466
|
+
CHECK_NOT_NULL(mik__http_st(mik_rt));
|
|
467
|
+
|
|
468
|
+
if (mik__http_st(mik_rt)->pending_count >= MIK_HTTP_MAX_PENDING) {
|
|
469
|
+
return mik__result_err_tag(ctx, "TooManyPending");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const char* url = JS_ToCString(ctx, argv[0]);
|
|
473
|
+
if (!url) return JS_EXCEPTION;
|
|
474
|
+
|
|
475
|
+
MIKHttpRequest req = {};
|
|
476
|
+
req.url = strdup(url);
|
|
477
|
+
JS_FreeCString(ctx, url);
|
|
478
|
+
if (!req.url) return JS_ThrowOutOfMemory(ctx);
|
|
479
|
+
req.method = HTTP_METHOD_GET;
|
|
480
|
+
|
|
481
|
+
if (argc > 1 && JS_IsObject(argv[1])) {
|
|
482
|
+
JSValue opts = argv[1];
|
|
483
|
+
|
|
484
|
+
JSValue method_val = JS_GetPropertyStr(ctx, opts, "method");
|
|
485
|
+
if (JS_IsString(method_val)) {
|
|
486
|
+
const char* method_str = JS_ToCString(ctx, method_val);
|
|
487
|
+
if (method_str) {
|
|
488
|
+
req.method = mik__method_from_string(method_str);
|
|
489
|
+
JS_FreeCString(ctx, method_str);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
JS_FreeValue(ctx, method_val);
|
|
493
|
+
|
|
494
|
+
JSValue body_val = JS_GetPropertyStr(ctx, opts, "body");
|
|
495
|
+
if (!JS_IsUndefined(body_val) && !JS_IsNull(body_val)) {
|
|
496
|
+
size_t body_size;
|
|
497
|
+
const uint8_t* body_ptr = JS_GetUint8Array(ctx, &body_size, body_val);
|
|
498
|
+
if (body_ptr && body_size > 0) {
|
|
499
|
+
req.body = static_cast<uint8_t*>(malloc(body_size));
|
|
500
|
+
if (!req.body) {
|
|
501
|
+
JS_FreeValue(ctx, body_val);
|
|
502
|
+
mik__http_free_request(&req);
|
|
503
|
+
return JS_ThrowOutOfMemory(ctx);
|
|
504
|
+
}
|
|
505
|
+
memcpy(req.body, body_ptr, body_size);
|
|
506
|
+
req.body_len = body_size;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
JS_FreeValue(ctx, body_val);
|
|
510
|
+
|
|
511
|
+
JSValue headers_val = JS_GetPropertyStr(ctx, opts, "headers");
|
|
512
|
+
if (JS_IsArray(headers_val)) {
|
|
513
|
+
JSValue len_val = JS_GetPropertyStr(ctx, headers_val, "length");
|
|
514
|
+
int64_t len = 0;
|
|
515
|
+
JS_ToInt64(ctx, &len, len_val);
|
|
516
|
+
JS_FreeValue(ctx, len_val);
|
|
517
|
+
|
|
518
|
+
if (len > 0) {
|
|
519
|
+
req.headers = static_cast<MIKHttpHeader*>(calloc(len, sizeof(MIKHttpHeader)));
|
|
520
|
+
if (!req.headers) {
|
|
521
|
+
JS_FreeValue(ctx, headers_val);
|
|
522
|
+
mik__http_free_request(&req);
|
|
523
|
+
return JS_ThrowOutOfMemory(ctx);
|
|
524
|
+
}
|
|
525
|
+
req.header_count = 0;
|
|
526
|
+
bool oom = false;
|
|
527
|
+
for (int64_t i = 0; i < len; i++) {
|
|
528
|
+
JSValue pair = JS_GetPropertyUint32(ctx, headers_val, i);
|
|
529
|
+
if (JS_IsArray(pair)) {
|
|
530
|
+
JSValue k = JS_GetPropertyUint32(ctx, pair, 0);
|
|
531
|
+
JSValue v = JS_GetPropertyUint32(ctx, pair, 1);
|
|
532
|
+
const char* ks = JS_ToCString(ctx, k);
|
|
533
|
+
const char* vs = JS_ToCString(ctx, v);
|
|
534
|
+
if (ks && vs) {
|
|
535
|
+
char* k_dup = strdup(ks);
|
|
536
|
+
char* v_dup = strdup(vs);
|
|
537
|
+
if (k_dup && v_dup) {
|
|
538
|
+
req.headers[req.header_count].key = k_dup;
|
|
539
|
+
req.headers[req.header_count].value = v_dup;
|
|
540
|
+
req.header_count++;
|
|
541
|
+
} else {
|
|
542
|
+
free(k_dup);
|
|
543
|
+
free(v_dup);
|
|
544
|
+
oom = true;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (ks) JS_FreeCString(ctx, ks);
|
|
548
|
+
if (vs) JS_FreeCString(ctx, vs);
|
|
549
|
+
JS_FreeValue(ctx, k);
|
|
550
|
+
JS_FreeValue(ctx, v);
|
|
551
|
+
}
|
|
552
|
+
JS_FreeValue(ctx, pair);
|
|
553
|
+
if (oom) break;
|
|
554
|
+
}
|
|
555
|
+
if (oom) {
|
|
556
|
+
JS_FreeValue(ctx, headers_val);
|
|
557
|
+
mik__http_free_request(&req);
|
|
558
|
+
return JS_ThrowOutOfMemory(ctx);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
JS_FreeValue(ctx, headers_val);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
MIKHttpPending pending = {};
|
|
566
|
+
pending.id = mik__http_st(mik_rt)->next_id++;
|
|
567
|
+
JSValue headers_promise = MIK_InitPromise(ctx, &pending.headers_promise);
|
|
568
|
+
|
|
569
|
+
auto* cancelled = new std::atomic<bool>(false);
|
|
570
|
+
pending.cancelled = cancelled;
|
|
571
|
+
|
|
572
|
+
auto* task_args = static_cast<MIKHttpTaskArgs*>(malloc(sizeof(MIKHttpTaskArgs)));
|
|
573
|
+
if (!task_args) {
|
|
574
|
+
mik__http_free_request(&req);
|
|
575
|
+
MIK_FreePromise(ctx, &pending.headers_promise);
|
|
576
|
+
delete cancelled;
|
|
577
|
+
return JS_ThrowOutOfMemory(ctx);
|
|
578
|
+
}
|
|
579
|
+
task_args->id = pending.id;
|
|
580
|
+
task_args->req = req;
|
|
581
|
+
task_args->result_queue = mik__http_st(mik_rt)->result_queue;
|
|
582
|
+
task_args->inflight = mik__http_st(mik_rt)->inflight;
|
|
583
|
+
task_args->cancelled = cancelled;
|
|
584
|
+
|
|
585
|
+
BaseType_t ret = xTaskCreate(mik__http_task, "mik_http", MIK_HTTP_TASK_STACK_SIZE, task_args,
|
|
586
|
+
tskIDLE_PRIORITY + 1, nullptr);
|
|
587
|
+
if (ret != pdPASS) {
|
|
588
|
+
mik__http_free_request(&task_args->req);
|
|
589
|
+
free(task_args);
|
|
590
|
+
delete cancelled;
|
|
591
|
+
MIK_FreePromise(ctx, &pending.headers_promise);
|
|
592
|
+
return mik__result_err_named(ctx, "Network",
|
|
593
|
+
"failed to create HTTP background task");
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
mik__http_st(mik_rt)->pending[mik__http_st(mik_rt)->pending_count++] = pending;
|
|
597
|
+
|
|
598
|
+
JSValue obj = JS_NewObject(ctx);
|
|
599
|
+
JS_DefinePropertyValueStr(ctx, obj, "ok", JS_TRUE, JS_PROP_C_W_E);
|
|
600
|
+
JS_DefinePropertyValueStr(ctx, obj, "id", JS_NewUint32(ctx, pending.id), JS_PROP_C_W_E);
|
|
601
|
+
JS_DefinePropertyValueStr(ctx, obj, "headers", headers_promise, JS_PROP_C_W_E);
|
|
602
|
+
return obj;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/* nextMessage(id) → Promise resolving with the next CHUNK/END/ERROR message. */
|
|
606
|
+
static JSValue mik__http_next_message(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
607
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
608
|
+
CHECK_NOT_NULL(mik_rt);
|
|
609
|
+
if (!mik__http_st(mik_rt)) {
|
|
610
|
+
JSValue v = mik__end_msg_value(ctx);
|
|
611
|
+
return MIK_NewResolvedPromise(ctx, 1, &v);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
uint32_t id;
|
|
615
|
+
if (JS_ToUint32(ctx, &id, argv[0])) return JS_EXCEPTION;
|
|
616
|
+
|
|
617
|
+
MIKHttpState* state = mik__http_st(mik_rt);
|
|
618
|
+
MIKHttpPending* p = mik__find_pending(state, id);
|
|
619
|
+
if (!p) {
|
|
620
|
+
/* Pending gone: request already finished. Return resolved END so
|
|
621
|
+
* iterators terminate cleanly on stray calls. */
|
|
622
|
+
JSValue v = mik__end_msg_value(ctx);
|
|
623
|
+
return MIK_NewResolvedPromise(ctx, 1, &v);
|
|
624
|
+
}
|
|
625
|
+
if (p->next_promise_active) {
|
|
626
|
+
/* Concurrent nextMessage calls are unsupported. */
|
|
627
|
+
return JS_ThrowTypeError(ctx, "nextMessage already pending for request %u", id);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/* Fast path: a message is already buffered. */
|
|
631
|
+
if (p->queue_head) {
|
|
632
|
+
MIKHttpQueuedMsg* q = p->queue_head;
|
|
633
|
+
p->queue_head = q->next;
|
|
634
|
+
if (!p->queue_head) p->queue_tail = nullptr;
|
|
635
|
+
|
|
636
|
+
JSValue v;
|
|
637
|
+
bool finished = false;
|
|
638
|
+
if (q->kind == MIK_HTTP_MSG_CHUNK) {
|
|
639
|
+
v = mik__chunk_msg_value(ctx, q->chunk_data, q->chunk_len);
|
|
640
|
+
xSemaphoreGive(state->inflight);
|
|
641
|
+
} else if (q->kind == MIK_HTTP_MSG_END) {
|
|
642
|
+
v = mik__end_msg_value(ctx);
|
|
643
|
+
finished = true;
|
|
644
|
+
} else {
|
|
645
|
+
v = mik__error_msg_value(ctx, q->is_cancelled, q->error_message);
|
|
646
|
+
finished = true;
|
|
647
|
+
}
|
|
648
|
+
mik__free_queued_msg(q);
|
|
649
|
+
JSValue promise = MIK_NewResolvedPromise(ctx, 1, &v);
|
|
650
|
+
if (finished) {
|
|
651
|
+
size_t idx = static_cast<size_t>(p - state->pending);
|
|
652
|
+
mik__pending_drop(state, idx);
|
|
653
|
+
}
|
|
654
|
+
return promise;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/* Slow path: wait. Create a new promise stored on the pending entry. */
|
|
658
|
+
JSValue promise = MIK_InitPromise(ctx, &p->next_promise);
|
|
659
|
+
p->next_promise_active = true;
|
|
660
|
+
return promise;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/* pendingCount() — number of in-flight requests whose terminal message has
|
|
664
|
+
* not yet been consumed. Useful for tests that want to verify cancel+drain
|
|
665
|
+
* freed a slot without relying on heap-headroom for a follow-up request. */
|
|
666
|
+
static JSValue mik__http_pending_count(JSContext* ctx, JSValue this_val, int argc,
|
|
667
|
+
JSValue* argv) {
|
|
668
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
669
|
+
CHECK_NOT_NULL(mik_rt);
|
|
670
|
+
if (!mik__http_st(mik_rt)) return JS_NewUint32(ctx, 0);
|
|
671
|
+
return JS_NewUint32(ctx, mik__http_st(mik_rt)->pending_count);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/* cancel(id) — signal a pending request to abort */
|
|
675
|
+
static JSValue mik__http_cancel(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
676
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
677
|
+
CHECK_NOT_NULL(mik_rt);
|
|
678
|
+
if (!mik__http_st(mik_rt)) return JS_UNDEFINED;
|
|
679
|
+
|
|
680
|
+
uint32_t id;
|
|
681
|
+
if (JS_ToUint32(ctx, &id, argv[0])) return JS_EXCEPTION;
|
|
682
|
+
|
|
683
|
+
MIKHttpState* state = mik__http_st(mik_rt);
|
|
684
|
+
for (size_t i = 0; i < state->pending_count; i++) {
|
|
685
|
+
if (state->pending[i].id == id) {
|
|
686
|
+
if (state->pending[i].cancelled) {
|
|
687
|
+
state->pending[i].cancelled->store(true, std::memory_order_relaxed);
|
|
688
|
+
}
|
|
689
|
+
state->pending[i].js_cancelled = true;
|
|
690
|
+
return JS_UNDEFINED;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return JS_UNDEFINED;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/* ── Module init ───────────────────────────────────────────────────── */
|
|
697
|
+
|
|
698
|
+
static void mik__ensure_netif_initialized() {
|
|
699
|
+
static bool initialized = false;
|
|
700
|
+
if (initialized) return;
|
|
701
|
+
initialized = true;
|
|
702
|
+
|
|
703
|
+
esp_err_t err = esp_netif_init();
|
|
704
|
+
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
|
705
|
+
ESP_LOGE(MIK_HTTP_TAG, "esp_netif_init failed: %s", esp_err_to_name(err));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
err = esp_event_loop_create_default();
|
|
709
|
+
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
|
710
|
+
ESP_LOGE(MIK_HTTP_TAG, "esp_event_loop_create_default failed: %s", esp_err_to_name(err));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
static void mik__http_ensure_initialized(JSContext* ctx) {
|
|
715
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
716
|
+
CHECK_NOT_NULL(mik_rt);
|
|
717
|
+
if (mik__http_st(mik_rt)) return;
|
|
718
|
+
|
|
719
|
+
mik__ensure_netif_initialized();
|
|
720
|
+
|
|
721
|
+
auto* state = new MIKHttpState();
|
|
722
|
+
/* Queue depth: enough for HEADERS + CHUNKs + END across all requests. */
|
|
723
|
+
state->result_queue =
|
|
724
|
+
xQueueCreate(MIK_HTTP_MAX_PENDING * 2 + MIK_HTTP_MAX_CHUNKS_INFLIGHT, sizeof(MIKHttpMsg));
|
|
725
|
+
state->inflight =
|
|
726
|
+
xSemaphoreCreateCounting(MIK_HTTP_MAX_CHUNKS_INFLIGHT, MIK_HTTP_MAX_CHUNKS_INFLIGHT);
|
|
727
|
+
CHECK_NOT_NULL(state->result_queue);
|
|
728
|
+
CHECK_NOT_NULL(state->inflight);
|
|
729
|
+
mik__http_st(mik_rt) = state;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
static int mik__http_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
733
|
+
mik__http_ensure_initialized(ctx);
|
|
734
|
+
JS_SetModuleExport(ctx, m, "request",
|
|
735
|
+
JS_NewCFunction(ctx, mik__http_request, "request", 2));
|
|
736
|
+
JS_SetModuleExport(ctx, m, "nextMessage",
|
|
737
|
+
JS_NewCFunction(ctx, mik__http_next_message, "nextMessage", 1));
|
|
738
|
+
JS_SetModuleExport(ctx, m, "cancel", JS_NewCFunction(ctx, mik__http_cancel, "cancel", 1));
|
|
739
|
+
JS_SetModuleExport(ctx, m, "pendingCount",
|
|
740
|
+
JS_NewCFunction(ctx, mik__http_pending_count, "pendingCount", 0));
|
|
741
|
+
return 0;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
static JSModuleDef* mik__http_init(JSContext* ctx) {
|
|
745
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
746
|
+
mik__http_slot = MIK_AllocModuleSlot(mik_rt);
|
|
747
|
+
|
|
748
|
+
JSModuleDef* m = JS_NewCModule(ctx, "native:http", mik__http_module_init);
|
|
749
|
+
if (!m) return nullptr;
|
|
750
|
+
JS_AddModuleExport(ctx, m, "request");
|
|
751
|
+
JS_AddModuleExport(ctx, m, "nextMessage");
|
|
752
|
+
JS_AddModuleExport(ctx, m, "cancel");
|
|
753
|
+
JS_AddModuleExport(ctx, m, "pendingCount");
|
|
754
|
+
return m;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/* ── Event loop consumption ────────────────────────────────────────── */
|
|
758
|
+
|
|
759
|
+
void mik__http_consume(JSContext* ctx) {
|
|
760
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
761
|
+
CHECK_NOT_NULL(mik_rt);
|
|
762
|
+
if (!mik__http_st(mik_rt)) return;
|
|
763
|
+
|
|
764
|
+
MIKHttpState* state = mik__http_st(mik_rt);
|
|
765
|
+
MIKHttpMsg msg;
|
|
766
|
+
while (xQueueReceive(state->result_queue, &msg, 0) == pdTRUE) {
|
|
767
|
+
MIKHttpPending* p = mik__find_pending(state, msg.id);
|
|
768
|
+
if (!p) {
|
|
769
|
+
/* Pending gone — free payloads and drop. */
|
|
770
|
+
if (msg.kind == MIK_HTTP_MSG_CHUNK) {
|
|
771
|
+
xSemaphoreGive(state->inflight);
|
|
772
|
+
free(msg.chunk_data);
|
|
773
|
+
} else if (msg.kind == MIK_HTTP_MSG_ERROR) {
|
|
774
|
+
free(msg.error_message);
|
|
775
|
+
} else if (msg.kind == MIK_HTTP_MSG_HEADERS) {
|
|
776
|
+
mik__http_free_headers(msg.headers, msg.header_count);
|
|
777
|
+
}
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (msg.kind == MIK_HTTP_MSG_HEADERS) {
|
|
782
|
+
JSValue v =
|
|
783
|
+
mik__headers_result_ok(ctx, msg.status, msg.headers, msg.header_count);
|
|
784
|
+
mik__http_free_headers(msg.headers, msg.header_count);
|
|
785
|
+
MIK_ResolvePromise(ctx, &p->headers_promise, 1, &v);
|
|
786
|
+
p->headers_resolved = true;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/* ERROR before HEADERS: deliver the failure via headers_promise so
|
|
791
|
+
* callers awaiting `start.headers` don't hang. The pending entry is
|
|
792
|
+
* then dropped; any queued messages for it are freed. */
|
|
793
|
+
if (msg.kind == MIK_HTTP_MSG_ERROR && !p->headers_resolved) {
|
|
794
|
+
const char* name = msg.is_cancelled ? "Aborted" : "Network";
|
|
795
|
+
/* If nextMessage was already awaiting, resolve it with the same
|
|
796
|
+
* error so the caller doesn't hang and the MIKPromise's JSValues
|
|
797
|
+
* are freed (MIK_ResolvePromise calls MIK_FreePromise internally). */
|
|
798
|
+
if (p->next_promise_active) {
|
|
799
|
+
JSValue em = mik__error_msg_value(ctx, msg.is_cancelled, msg.error_message);
|
|
800
|
+
MIK_ResolvePromise(ctx, &p->next_promise, 1, &em);
|
|
801
|
+
p->next_promise_active = false;
|
|
802
|
+
}
|
|
803
|
+
JSValue v = mik__result_err_named(ctx, name, "%s",
|
|
804
|
+
msg.error_message ? msg.error_message : "HTTP error");
|
|
805
|
+
free(msg.error_message);
|
|
806
|
+
MIK_ResolvePromise(ctx, &p->headers_promise, 1, &v);
|
|
807
|
+
p->headers_resolved = true;
|
|
808
|
+
size_t idx = static_cast<size_t>(p - state->pending);
|
|
809
|
+
mik__pending_drop(state, idx);
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/* CHUNK / END / ERROR: deliver or enqueue. */
|
|
814
|
+
if (p->next_promise_active) {
|
|
815
|
+
size_t idx = static_cast<size_t>(p - state->pending);
|
|
816
|
+
bool finished = false;
|
|
817
|
+
mik__resolve_next(ctx, p, msg.kind, msg.chunk_data, msg.chunk_len, msg.is_cancelled,
|
|
818
|
+
msg.error_message, &finished);
|
|
819
|
+
if (msg.kind == MIK_HTTP_MSG_CHUNK) {
|
|
820
|
+
xSemaphoreGive(state->inflight);
|
|
821
|
+
free(msg.chunk_data);
|
|
822
|
+
} else if (msg.kind == MIK_HTTP_MSG_ERROR) {
|
|
823
|
+
free(msg.error_message);
|
|
824
|
+
}
|
|
825
|
+
if (finished) mik__pending_drop(state, idx);
|
|
826
|
+
} else if ((msg.kind == MIK_HTTP_MSG_END || msg.kind == MIK_HTTP_MSG_ERROR) &&
|
|
827
|
+
p->js_cancelled) {
|
|
828
|
+
/* Terminal message for a cancelled request with no JS consumer.
|
|
829
|
+
* Safe to discard: nobody will iterate this body. */
|
|
830
|
+
if (msg.kind == MIK_HTTP_MSG_ERROR) free(msg.error_message);
|
|
831
|
+
size_t idx = static_cast<size_t>(p - state->pending);
|
|
832
|
+
mik__pending_drop(state, idx);
|
|
833
|
+
} else {
|
|
834
|
+
if (!mik__enqueue_msg(p, msg)) {
|
|
835
|
+
/* OOM: drop this frame. Mirror the delivery-path cleanup so
|
|
836
|
+
* the inflight semaphore and payload don't leak. A dropped
|
|
837
|
+
* terminal message can hang a JS iterator — callers should
|
|
838
|
+
* apply fetch() timeoutMs to bound the wait. */
|
|
839
|
+
if (msg.kind == MIK_HTTP_MSG_CHUNK) {
|
|
840
|
+
xSemaphoreGive(state->inflight);
|
|
841
|
+
free(msg.chunk_data);
|
|
842
|
+
} else if (msg.kind == MIK_HTTP_MSG_ERROR) {
|
|
843
|
+
free(msg.error_message);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/* chunk_data / error_message ownership transferred to queue entry
|
|
847
|
+
* (or already freed on the OOM drop above). */
|
|
848
|
+
/* Terminal message means the background task has exited its loop
|
|
849
|
+
* and will never read the shared cancelled atomic again. Free and
|
|
850
|
+
* null it so destroy/cancel don't touch stale memory. */
|
|
851
|
+
if (msg.kind == MIK_HTTP_MSG_END || msg.kind == MIK_HTTP_MSG_ERROR) {
|
|
852
|
+
delete p->cancelled;
|
|
853
|
+
p->cancelled = nullptr;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
void mik__http_destroy(JSContext* ctx) {
|
|
860
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
861
|
+
CHECK_NOT_NULL(mik_rt);
|
|
862
|
+
if (!mik__http_st(mik_rt)) return;
|
|
863
|
+
|
|
864
|
+
MIKHttpState* state = mik__http_st(mik_rt);
|
|
865
|
+
|
|
866
|
+
/* Signal all pending requests to cancel so background tasks clean up. */
|
|
867
|
+
size_t remaining = 0;
|
|
868
|
+
for (size_t i = 0; i < state->pending_count; i++) {
|
|
869
|
+
if (state->pending[i].cancelled) {
|
|
870
|
+
state->pending[i].cancelled->store(true, std::memory_order_relaxed);
|
|
871
|
+
state->pending[i].js_cancelled = true;
|
|
872
|
+
remaining++;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/* Drain until every pending task has emitted its END/ERROR message. */
|
|
877
|
+
while (remaining > 0) {
|
|
878
|
+
MIKHttpMsg msg;
|
|
879
|
+
if (xQueueReceive(state->result_queue, &msg, pdMS_TO_TICKS(1000)) != pdTRUE) continue;
|
|
880
|
+
if (msg.kind == MIK_HTTP_MSG_CHUNK) {
|
|
881
|
+
xSemaphoreGive(state->inflight);
|
|
882
|
+
free(msg.chunk_data);
|
|
883
|
+
} else if (msg.kind == MIK_HTTP_MSG_ERROR) {
|
|
884
|
+
free(msg.error_message);
|
|
885
|
+
remaining--;
|
|
886
|
+
} else if (msg.kind == MIK_HTTP_MSG_END) {
|
|
887
|
+
remaining--;
|
|
888
|
+
} else if (msg.kind == MIK_HTTP_MSG_HEADERS) {
|
|
889
|
+
mik__http_free_headers(msg.headers, msg.header_count);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/* Free any remaining pending promises and queues. MIK_ResolvePromise
|
|
894
|
+
* already frees the MIKPromise's JSValues on settlement, so only free
|
|
895
|
+
* promises that were never resolved. */
|
|
896
|
+
for (size_t i = 0; i < state->pending_count; i++) {
|
|
897
|
+
MIKHttpPending* p = &state->pending[i];
|
|
898
|
+
delete p->cancelled;
|
|
899
|
+
p->cancelled = nullptr;
|
|
900
|
+
MIKHttpQueuedMsg* m = p->queue_head;
|
|
901
|
+
while (m) {
|
|
902
|
+
MIKHttpQueuedMsg* next = m->next;
|
|
903
|
+
mik__free_queued_msg(m);
|
|
904
|
+
m = next;
|
|
905
|
+
}
|
|
906
|
+
if (!p->headers_resolved) MIK_FreePromise(ctx, &p->headers_promise);
|
|
907
|
+
if (p->next_promise_active) MIK_FreePromise(ctx, &p->next_promise);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
vQueueDelete(state->result_queue);
|
|
911
|
+
vSemaphoreDelete(state->inflight);
|
|
912
|
+
delete state;
|
|
913
|
+
mik__http_st(mik_rt) = nullptr;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
MIK_REGISTER_MODULE(http, "native:http", mik__http_init, mik__http_consume, mik__http_destroy)
|