xbintsc 0.5.20 → 0.5.21

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 (100) hide show
  1. package/AGENTS.md +5 -3
  2. package/dist/src/driver/compiler.js +1 -1
  3. package/dist/src/driver/compiler.js.map +1 -1
  4. package/dist/src/driver/runtime-objects.d.ts +1 -1
  5. package/dist/src/driver/runtime-objects.js +1 -0
  6. package/dist/src/driver/runtime-objects.js.map +1 -1
  7. package/dist/src/driver/version.d.ts +1 -1
  8. package/dist/src/driver/version.js +1 -1
  9. package/dist/src/extensions/node/fs/index.d.ts +4 -3
  10. package/dist/src/extensions/node/fs/index.js +12 -3
  11. package/dist/src/extensions/node/fs/index.js.map +1 -1
  12. package/dist/src/extensions/node/fs-promises/index.d.ts +5 -5
  13. package/dist/src/extensions/node/fs-promises/index.js +15 -6
  14. package/dist/src/extensions/node/fs-promises/index.js.map +1 -1
  15. package/dist/tests/e2e/async-await.test.js +1 -1
  16. package/dist/tests/e2e/async-fs.test.d.ts +9 -0
  17. package/dist/tests/e2e/async-fs.test.js +119 -0
  18. package/dist/tests/e2e/async-fs.test.js.map +1 -0
  19. package/dist/tests/e2e/fetch.test.js +24 -0
  20. package/dist/tests/e2e/fetch.test.js.map +1 -1
  21. package/dist/tests/e2e/gui-inline-svg.test.d.ts +11 -0
  22. package/dist/tests/e2e/gui-inline-svg.test.js +160 -0
  23. package/dist/tests/e2e/gui-inline-svg.test.js.map +1 -0
  24. package/doc/ai/contributing.md +2 -1
  25. package/doc/ai/extensions.md +4 -3
  26. package/doc/ai/language-support.md +2 -1
  27. package/doc/ai/zh-CN/contributing.md +1 -1
  28. package/doc/ai/zh-CN/extensions.md +4 -3
  29. package/doc/ai/zh-CN/language-support.md +2 -1
  30. package/doc/gui/api.md +8 -0
  31. package/doc/gui/known-issues.md +10 -9
  32. package/doc/gui/media.md +32 -3
  33. package/doc/gui.md +24 -2
  34. package/doc/implemented/async.md +16 -3
  35. package/doc/implemented/fetch.md +38 -6
  36. package/doc/implemented/node/fs-promises.md +19 -8
  37. package/doc/implemented/node.md +1 -1
  38. package/doc/implemented.md +1 -1
  39. package/doc/unimplemented/deviations.md +1 -1
  40. package/doc/unimplemented/node/async.md +6 -3
  41. package/doc/unimplemented/node/fs-promises.md +10 -6
  42. package/doc/unimplemented/node/fs.md +7 -8
  43. package/doc/unimplemented/node.md +2 -2
  44. package/doc/unimplemented/toolchain.md +1 -1
  45. package/doc/zh-CN/gui/api.md +7 -0
  46. package/doc/zh-CN/gui/known-issues.md +8 -7
  47. package/doc/zh-CN/gui/media.md +26 -2
  48. package/doc/zh-CN/gui.md +18 -1
  49. package/doc/zh-CN/implemented/async.md +13 -2
  50. package/doc/zh-CN/implemented/fetch.md +23 -4
  51. package/doc/zh-CN/implemented/node/fs-promises.md +6 -3
  52. package/doc/zh-CN/implemented/node.md +1 -1
  53. package/doc/zh-CN/implemented.md +1 -1
  54. package/doc/zh-CN/unimplemented/deviations.md +1 -1
  55. package/doc/zh-CN/unimplemented/node/async.md +6 -3
  56. package/doc/zh-CN/unimplemented/node/fs-promises.md +4 -4
  57. package/doc/zh-CN/unimplemented/node/fs.md +2 -2
  58. package/doc/zh-CN/unimplemented/node.md +2 -2
  59. package/doc/zh-CN/unimplemented/toolchain.md +1 -1
  60. package/package.json +1 -1
  61. package/runtime/ext_gui/inline_svg.cpp +219 -0
  62. package/runtime/ext_gui/inline_svg.h +41 -0
  63. package/runtime/ext_gui/layout.cpp +18 -0
  64. package/runtime/ext_gui/layout.h +7 -0
  65. package/runtime/ext_gui/layout_flow.cpp +4 -3
  66. package/runtime/ext_gui/paint.cpp +14 -0
  67. package/runtime/ext_gui/paint.h +5 -1
  68. package/runtime/ext_gui/renderer_image.cpp +53 -25
  69. package/runtime/ext_gui/style_ua.cpp +3 -0
  70. package/runtime/ext_gui/svg.cpp +122 -39
  71. package/runtime/ext_gui/svg.h +14 -3
  72. package/runtime/ext_gui/window.cpp +28 -0
  73. package/runtime/ext_node/fs/async_api.c +314 -0
  74. package/runtime/ext_node/fs/async_complete.c +99 -0
  75. package/runtime/ext_node/fs/async_fh.c +206 -0
  76. package/runtime/ext_node/fs/async_job.c +81 -0
  77. package/runtime/ext_node/fs/async_job.h +132 -0
  78. package/runtime/ext_node/fs/async_worker.c +534 -0
  79. package/runtime/ext_node/fs/async_worker_dir.c +439 -0
  80. package/runtime/ext_node/fs/async_worker_dir.h +33 -0
  81. package/runtime/ext_node/fs/async_worker_fd.c +74 -0
  82. package/runtime/ext_node/fs/dir.c +60 -15
  83. package/runtime/ext_node/fs/fs_common.h +9 -0
  84. package/runtime/ext_node/fs/fs_ops.c +18 -29
  85. package/runtime/ext_node/fs/parts/builtins.inc +34 -0
  86. package/runtime/ext_node/fs/parts/platform.inc +66 -0
  87. package/runtime/ext_node/fs/promises.c +44 -234
  88. package/runtime/rt_internal.h +2 -0
  89. package/runtime/xt_async_io.c +333 -0
  90. package/runtime/xt_async_io.h +47 -0
  91. package/runtime/xt_fetch/async.inc +295 -0
  92. package/runtime/xt_fetch/request.inc +6 -183
  93. package/runtime/xt_fetch/socket.inc +5 -4
  94. package/runtime/xt_fetch.c +6 -4
  95. package/runtime/xt_loop.c +44 -3
  96. package/src/driver/compiler.ts +1 -1
  97. package/src/driver/runtime-objects.ts +1 -0
  98. package/src/driver/version.ts +1 -1
  99. package/src/extensions/node/fs/index.ts +12 -3
  100. package/src/extensions/node/fs-promises/index.ts +15 -6
@@ -0,0 +1,333 @@
1
+ /*
2
+ * xbintsc runtime — asynchronous I/O offload pool.
3
+ *
4
+ * JavaScript's `await` can suspend an `async` body on a pending promise, but
5
+ * the host still has to provide the work that eventually settles it. Sockets
6
+ * and timers are readiness based and live directly on the `select(2)` reactor;
7
+ * file I/O and DNS have no portable readiness notification, so they run on a
8
+ * small pool of worker threads and report back through the same reactor.
9
+ *
10
+ * The threading contract is the important part:
11
+ *
12
+ * - `job->run` executes on a worker thread and may only touch plain C memory
13
+ * (paths, buffers, syscalls). It must never call the `xt_*` value API, the
14
+ * collector, `xt_throw`, or the event loop.
15
+ * - `job->complete` executes on the main thread after the reactor wakes up.
16
+ * That is where results become `xt_value`s, promises settle, and a job may
17
+ * submit follow-up work (fetch redirects do exactly that).
18
+ * - `job->roots` keeps values needed across the suspension alive. They are
19
+ * written on the main thread and scanned by the collector like any other
20
+ * root.
21
+ *
22
+ * Completion wakes the reactor through a self-pipe (POSIX) or a loopback UDP
23
+ * socket (Windows, whose `select` only understands sockets). The reactor also
24
+ * polls the pool every iteration, so a failed wakeup degrades to a short wait
25
+ * rather than a hang.
26
+ */
27
+
28
+ #include "rt_internal.h"
29
+
30
+ #include <stdlib.h>
31
+ #include <string.h>
32
+
33
+ #if defined(_WIN32)
34
+ #include <winsock2.h>
35
+ #include <windows.h>
36
+ #include <process.h>
37
+ #else
38
+ #include <errno.h>
39
+ #include <fcntl.h>
40
+ #include <pthread.h>
41
+ #include <unistd.h>
42
+ #endif
43
+
44
+ #define XT_ASYNC_WORKERS 4
45
+ /* A fallback bound for the reactor wait while work is outstanding. With a
46
+ * working wakeup the wait ends immediately when a job completes. */
47
+ #define XT_ASYNC_POLL_MS 10
48
+
49
+ static xt_async_job *xt_async_work_head = NULL;
50
+ static xt_async_job *xt_async_work_tail = NULL;
51
+ static xt_async_job *xt_async_done_head = NULL;
52
+ static xt_async_job *xt_async_done_tail = NULL;
53
+
54
+ /* Every submitted, not-yet-released job. Mutated only on the main thread. */
55
+ static xt_async_job *xt_async_live_head = NULL;
56
+
57
+ static int xt_async_pending_jobs = 0;
58
+ static int xt_async_workers_started = 0;
59
+ static int xt_async_worker_count = 0;
60
+ static int xt_async_roots_registered = 0;
61
+ static int xt_async_watcher_fd = -1;
62
+
63
+ #if defined(_WIN32)
64
+ /* Statically initialised like the POSIX primitives below: the reactor polls
65
+ * the pool on every timer/socket iteration, even when no async job was ever
66
+ * submitted, so the lock must be usable before `xt_async_submit` runs. */
67
+ static SRWLOCK xt_async_lock = SRWLOCK_INIT;
68
+ static CONDITION_VARIABLE xt_async_cond = CONDITION_VARIABLE_INIT;
69
+ static SOCKET xt_async_wake_socket = INVALID_SOCKET;
70
+ #define XT_ASYNC_LOCK() AcquireSRWLockExclusive(&xt_async_lock)
71
+ #define XT_ASYNC_UNLOCK() ReleaseSRWLockExclusive(&xt_async_lock)
72
+ static void xt_async_cond_wait(void) {
73
+ SleepConditionVariableSRW(&xt_async_cond, &xt_async_lock, INFINITE, 0);
74
+ }
75
+ static void xt_async_cond_signal(void) { WakeConditionVariable(&xt_async_cond); }
76
+ #else
77
+ static pthread_mutex_t xt_async_lock = PTHREAD_MUTEX_INITIALIZER;
78
+ static pthread_cond_t xt_async_cond = PTHREAD_COND_INITIALIZER;
79
+ static int xt_async_wake_pipe[2] = {-1, -1};
80
+ #define XT_ASYNC_LOCK() pthread_mutex_lock(&xt_async_lock)
81
+ #define XT_ASYNC_UNLOCK() pthread_mutex_unlock(&xt_async_lock)
82
+ static void xt_async_cond_wait(void) { pthread_cond_wait(&xt_async_cond, &xt_async_lock); }
83
+ static void xt_async_cond_signal(void) { pthread_cond_signal(&xt_async_cond); }
84
+ #endif
85
+
86
+ static void xt_async_scan_roots(void);
87
+
88
+ /* -- wakeup channel ------------------------------------------------------- */
89
+
90
+ static int xt_async_wake_fd(void) {
91
+ #if defined(_WIN32)
92
+ return (int)xt_async_wake_socket;
93
+ #else
94
+ return xt_async_wake_pipe[0];
95
+ #endif
96
+ }
97
+
98
+ static void xt_async_wake_create(void) {
99
+ #if defined(_WIN32)
100
+ if (xt_async_wake_socket != INVALID_SOCKET) return;
101
+ SOCKET socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
102
+ if (socket_fd == INVALID_SOCKET) return;
103
+ struct sockaddr_in address;
104
+ memset(&address, 0, sizeof(address));
105
+ address.sin_family = AF_INET;
106
+ address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
107
+ address.sin_port = 0;
108
+ if (bind(socket_fd, (struct sockaddr *)&address, sizeof(address)) != 0) {
109
+ closesocket(socket_fd);
110
+ return;
111
+ }
112
+ int length = (int)sizeof(address);
113
+ if (getsockname(socket_fd, (struct sockaddr *)&address, &length) != 0 ||
114
+ connect(socket_fd, (struct sockaddr *)&address, sizeof(address)) != 0) {
115
+ closesocket(socket_fd);
116
+ return;
117
+ }
118
+ u_long nonBlocking = 1;
119
+ ioctlsocket(socket_fd, FIONBIO, &nonBlocking);
120
+ xt_async_wake_socket = socket_fd;
121
+ #else
122
+ if (xt_async_wake_pipe[0] >= 0) return;
123
+ if (pipe(xt_async_wake_pipe) != 0) {
124
+ xt_async_wake_pipe[0] = -1;
125
+ xt_async_wake_pipe[1] = -1;
126
+ return;
127
+ }
128
+ for (int i = 0; i < 2; i++) {
129
+ int flags = fcntl(xt_async_wake_pipe[i], F_GETFL, 0);
130
+ if (flags >= 0) fcntl(xt_async_wake_pipe[i], F_SETFL, flags | O_NONBLOCK);
131
+ }
132
+ #endif
133
+ }
134
+
135
+ /* Called by a worker after it queues a completion. */
136
+ static void xt_async_wakeup(void) {
137
+ #if defined(_WIN32)
138
+ if (xt_async_wake_socket == INVALID_SOCKET) return;
139
+ char byte = 1;
140
+ send(xt_async_wake_socket, &byte, 1, 0);
141
+ #else
142
+ if (xt_async_wake_pipe[1] < 0) return;
143
+ char byte = 1;
144
+ ssize_t ignored = write(xt_async_wake_pipe[1], &byte, 1);
145
+ (void)ignored;
146
+ #endif
147
+ }
148
+
149
+ static void xt_async_wake_drain(void) {
150
+ #if defined(_WIN32)
151
+ if (xt_async_wake_socket == INVALID_SOCKET) return;
152
+ char buffer[256];
153
+ while (recv(xt_async_wake_socket, buffer, (int)sizeof(buffer), 0) > 0) {
154
+ }
155
+ #else
156
+ if (xt_async_wake_pipe[0] < 0) return;
157
+ char buffer[256];
158
+ while (read(xt_async_wake_pipe[0], buffer, sizeof(buffer)) > 0) {
159
+ }
160
+ #endif
161
+ }
162
+
163
+ static void xt_async_wake_handler(void *userdata, int events) {
164
+ (void)userdata;
165
+ (void)events;
166
+ xt_async_poll();
167
+ }
168
+
169
+ /* -- queue helpers (lock held) -------------------------------------------- */
170
+
171
+ static void xt_async_push_work(xt_async_job *job) {
172
+ job->next = NULL;
173
+ if (xt_async_work_tail) xt_async_work_tail->next = job;
174
+ else xt_async_work_head = job;
175
+ xt_async_work_tail = job;
176
+ }
177
+
178
+ static void xt_async_push_done(xt_async_job *job) {
179
+ job->next = NULL;
180
+ if (xt_async_done_tail) xt_async_done_tail->next = job;
181
+ else xt_async_done_head = job;
182
+ xt_async_done_tail = job;
183
+ }
184
+
185
+ static xt_async_job *xt_async_pop_work(void) {
186
+ xt_async_job *job = xt_async_work_head;
187
+ if (!job) return NULL;
188
+ xt_async_work_head = job->next;
189
+ if (!xt_async_work_head) xt_async_work_tail = NULL;
190
+ return job;
191
+ }
192
+
193
+ static xt_async_job *xt_async_pop_done(void) {
194
+ xt_async_job *job = xt_async_done_head;
195
+ if (!job) return NULL;
196
+ xt_async_done_head = job->next;
197
+ if (!xt_async_done_head) xt_async_done_tail = NULL;
198
+ return job;
199
+ }
200
+
201
+ /* -- live job registry (main thread only) --------------------------------- */
202
+
203
+ static void xt_async_live_add(xt_async_job *job) {
204
+ job->live_prev = NULL;
205
+ job->live_next = xt_async_live_head;
206
+ if (xt_async_live_head) xt_async_live_head->live_prev = job;
207
+ xt_async_live_head = job;
208
+ }
209
+
210
+ static void xt_async_live_remove(xt_async_job *job) {
211
+ if (job->live_prev) job->live_prev->live_next = job->live_next;
212
+ else xt_async_live_head = job->live_next;
213
+ if (job->live_next) job->live_next->live_prev = job->live_prev;
214
+ job->live_next = NULL;
215
+ job->live_prev = NULL;
216
+ }
217
+
218
+ static void xt_async_scan_roots(void) {
219
+ for (xt_async_job *job = xt_async_live_head; job; job = job->live_next) {
220
+ for (int i = 0; i < XT_ASYNC_ROOT_SLOTS; i++) xt_gc_mark_value(job->roots[i]);
221
+ }
222
+ }
223
+
224
+ static void xt_async_register_roots(void) {
225
+ if (xt_async_roots_registered) return;
226
+ xt_async_roots_registered = 1;
227
+ xt_gc_register_root_provider(xt_async_scan_roots);
228
+ }
229
+
230
+ /* -- worker threads -------------------------------------------------------- */
231
+
232
+ static void xt_async_worker_run(void) {
233
+ for (;;) {
234
+ XT_ASYNC_LOCK();
235
+ xt_async_job *job = xt_async_pop_work();
236
+ while (!job) {
237
+ xt_async_cond_wait();
238
+ job = xt_async_pop_work();
239
+ }
240
+ XT_ASYNC_UNLOCK();
241
+ if (job->run) job->run(job);
242
+ XT_ASYNC_LOCK();
243
+ xt_async_push_done(job);
244
+ XT_ASYNC_UNLOCK();
245
+ xt_async_wakeup();
246
+ }
247
+ }
248
+
249
+ #if defined(_WIN32)
250
+ static unsigned __stdcall xt_async_worker_entry(void *argument) {
251
+ (void)argument;
252
+ xt_async_worker_run();
253
+ return 0;
254
+ }
255
+ #else
256
+ static void *xt_async_worker_entry(void *argument) {
257
+ (void)argument;
258
+ xt_async_worker_run();
259
+ return NULL;
260
+ }
261
+ #endif
262
+
263
+ /* Spawn the pool lazily. Called with the lock held so a worker that starts
264
+ * immediately blocks until the queue is published. */
265
+ static void xt_async_start_workers(void) {
266
+ if (xt_async_workers_started) return;
267
+ xt_async_workers_started = 1;
268
+ for (int i = 0; i < XT_ASYNC_WORKERS; i++) {
269
+ #if defined(_WIN32)
270
+ uintptr_t handle = _beginthreadex(NULL, 0, xt_async_worker_entry, NULL, 0, NULL);
271
+ if (handle == 0) break;
272
+ CloseHandle((HANDLE)handle);
273
+ #else
274
+ pthread_t thread;
275
+ if (pthread_create(&thread, NULL, xt_async_worker_entry, NULL) != 0) break;
276
+ pthread_detach(thread);
277
+ #endif
278
+ xt_async_worker_count++;
279
+ }
280
+ }
281
+
282
+ /* -- public API ------------------------------------------------------------ */
283
+
284
+ int xt_async_pending(void) { return xt_async_pending_jobs; }
285
+
286
+ void xt_async_submit(xt_async_job *job) {
287
+ if (!job || !job->run) {
288
+ if (job && job->release) job->release(job);
289
+ else free(job);
290
+ return;
291
+ }
292
+ xt_async_register_roots();
293
+ xt_async_live_add(job);
294
+ xt_async_pending_jobs++;
295
+
296
+ xt_async_wake_create();
297
+ if (xt_async_watcher_fd < 0 && xt_async_wake_fd() >= 0) {
298
+ if (xt_loop_add(xt_async_wake_fd(), XT_IO_READ, xt_async_wake_handler, NULL) == 0) {
299
+ xt_async_watcher_fd = xt_async_wake_fd();
300
+ }
301
+ }
302
+
303
+ XT_ASYNC_LOCK();
304
+ xt_async_start_workers();
305
+ if (xt_async_worker_count > 0) {
306
+ xt_async_push_work(job);
307
+ xt_async_cond_signal();
308
+ XT_ASYNC_UNLOCK();
309
+ return;
310
+ }
311
+ /* No worker could be created: complete on the reactor's next poll. */
312
+ xt_async_push_done(job);
313
+ XT_ASYNC_UNLOCK();
314
+ }
315
+
316
+ void xt_async_poll(void) {
317
+ if (xt_async_watcher_fd >= 0) xt_async_wake_drain();
318
+ for (;;) {
319
+ XT_ASYNC_LOCK();
320
+ xt_async_job *job = xt_async_pop_done();
321
+ XT_ASYNC_UNLOCK();
322
+ if (!job) break;
323
+ if (job->complete) job->complete(job);
324
+ xt_async_live_remove(job);
325
+ if (job->release) job->release(job);
326
+ else free(job);
327
+ xt_async_pending_jobs--;
328
+ }
329
+ if (xt_async_pending_jobs == 0 && xt_async_watcher_fd >= 0) {
330
+ xt_loop_remove(xt_async_watcher_fd);
331
+ xt_async_watcher_fd = -1;
332
+ }
333
+ }
@@ -0,0 +1,47 @@
1
+ /*
2
+ * xbintsc runtime — asynchronous I/O offload API.
3
+ *
4
+ * Declared separately from `rt_internal.h` so the internal header stays within
5
+ * the repository's file-size budget. Extensions that offload blocking work
6
+ * include `rt_internal.h`, which includes this header.
7
+ */
8
+ #ifndef XT_RT_ASYNC_IO_H
9
+ #define XT_RT_ASYNC_IO_H
10
+
11
+ #include "rt.h"
12
+
13
+ /*
14
+ * Some host operations (file I/O, DNS, a blocking HTTP exchange) have no
15
+ * portable readiness notification, so they run on a small worker pool and
16
+ * report back to the main thread. The contract is strict: `run` executes on a
17
+ * worker and may only touch plain C memory, while `complete` executes on the
18
+ * main thread and may build/consume `xt_value`s.
19
+ *
20
+ * A job begins with this struct. `roots` keeps up to four values alive while
21
+ * the job is queued or running: the slots are scanned by the collector and are
22
+ * only written from the main thread. `next` links the work/completion queue and
23
+ * is owned by the pool; callers never touch it. `release` (optional) frees the
24
+ * job after `complete` returns.
25
+ */
26
+ #define XT_ASYNC_ROOT_SLOTS 4
27
+
28
+ typedef struct xt_async_job xt_async_job;
29
+
30
+ struct xt_async_job {
31
+ xt_async_job *next;
32
+ xt_async_job *live_next;
33
+ xt_async_job *live_prev;
34
+ void (*run)(xt_async_job *job);
35
+ void (*complete)(xt_async_job *job);
36
+ void (*release)(xt_async_job *job);
37
+ xt_value roots[XT_ASYNC_ROOT_SLOTS];
38
+ };
39
+
40
+ /** Queue `job` on a worker (never blocks the caller). Takes ownership. */
41
+ void xt_async_submit(xt_async_job *job);
42
+ /** Number of submitted jobs whose completion has not run yet. */
43
+ int xt_async_pending(void);
44
+ /** Run completed jobs and drop the loop watcher when the queue becomes idle. */
45
+ void xt_async_poll(void);
46
+
47
+ #endif /* XT_RT_ASYNC_IO_H */
@@ -0,0 +1,295 @@
1
+ /* -- asynchronous request/response exchange ------------------------------- */
2
+
3
+ /*
4
+ * `fetch` is a state machine over the async I/O pool. The main thread validates
5
+ * the URL, turns `init.headers` into a `Headers` object and serialises the
6
+ * request; a worker performs the blocking `getaddrinfo`/connect/send/recv
7
+ * exchange; the main thread parses the raw response, follows redirects and
8
+ * builds the `Response`. Nothing observed from JavaScript blocks the reactor, so
9
+ * timers and sockets keep running while a request is in flight.
10
+ *
11
+ * The worker only sees plain C memory. The values that must survive across the
12
+ * suspension (`promise`, `headers`) are held in the job's `roots` slots and
13
+ * scanned by the collector.
14
+ */
15
+
16
+ typedef struct xt_fetch_state xt_fetch_state;
17
+
18
+ typedef struct {
19
+ xt_async_job base;
20
+ xt_fetch_state *state;
21
+ char *request;
22
+ size_t request_length;
23
+ xt_fetch_buffer raw;
24
+ int failed;
25
+ } xt_fetch_hop;
26
+
27
+ struct xt_fetch_state {
28
+ xt_value promise;
29
+ xt_value headers_source;
30
+ char *url;
31
+ char method[16];
32
+ char redirect[16];
33
+ xt_fetch_buffer body;
34
+ int redirects;
35
+ };
36
+
37
+ static void xt_fetch_start(xt_fetch_state *state);
38
+
39
+ static void xt_fetch_state_free(xt_fetch_state *state) {
40
+ if (!state) return;
41
+ free(state->url);
42
+ xt_fetch_buffer_free(&state->body);
43
+ free(state);
44
+ }
45
+
46
+ static void xt_fetch_state_reject(xt_fetch_state *state, const char *message) {
47
+ xt_value argument = xt_string_from_cstr(message);
48
+ xt_value error = xt_type_error_ctor(1, &argument);
49
+ xt_value promise = state->promise;
50
+ xt_fetch_state_free(state);
51
+ xt_promise_reject_value(promise, error);
52
+ }
53
+
54
+ static void xt_fetch_hop_release(xt_async_job *job) {
55
+ xt_fetch_hop *hop = (xt_fetch_hop *)job;
56
+ free(hop->request);
57
+ xt_fetch_buffer_free(&hop->raw);
58
+ free(hop);
59
+ }
60
+
61
+ /* Worker thread: connect, write the request, read until the response is
62
+ * complete (or the peer closes). Only plain C memory is touched here. */
63
+ static void xt_fetch_hop_run(xt_async_job *job) {
64
+ xt_fetch_hop *hop = (xt_fetch_hop *)job;
65
+ xt_fetch_url parsed;
66
+ if (xt_fetch_parse_url(hop->state->url, &parsed) != 0) {
67
+ hop->failed = 1;
68
+ return;
69
+ }
70
+ int fd = xt_fetch_connect(&parsed);
71
+ if (fd < 0) {
72
+ hop->failed = 1;
73
+ return;
74
+ }
75
+ if (xt_fetch_send_all(fd, hop->request, hop->request_length) != 0) {
76
+ xt_fetch_close_socket(fd);
77
+ hop->failed = 1;
78
+ return;
79
+ }
80
+
81
+ char chunk[16384];
82
+ for (;;) {
83
+ int headEnd = xt_fetch_head_end(&hop->raw);
84
+ if (headEnd > 0) {
85
+ int status = 0;
86
+ char statusText[256];
87
+ size_t headerStart = 0;
88
+ if (xt_fetch_parse_status_line(hop->raw.data, hop->raw.length, &status, statusText, sizeof(statusText),
89
+ &headerStart) == 0) {
90
+ int stop = 0;
91
+ if (status == 204 || status == 304 || (status >= 100 && status < 200) ||
92
+ strcmp(hop->state->method, "HEAD") == 0) {
93
+ stop = 1;
94
+ } else {
95
+ size_t contentLength = 0;
96
+ int hasContentLength = 0;
97
+ int chunked = 0;
98
+ xt_fetch_scan_headers(hop->raw.data, headerStart, (size_t)headEnd - 2, &contentLength, &hasContentLength,
99
+ &chunked);
100
+ if (chunked) {
101
+ size_t decoded = 0;
102
+ if (xt_fetch_chunked_complete(hop->raw.data, hop->raw.length, (size_t)headEnd, &decoded) == 1) stop = 1;
103
+ } else if (hasContentLength && hop->raw.length - (size_t)headEnd >= contentLength) {
104
+ stop = 1;
105
+ }
106
+ }
107
+ if (stop) break;
108
+ }
109
+ }
110
+ int count = xt_fetch_recv(fd, chunk, sizeof(chunk));
111
+ if (count > 0) {
112
+ xt_fetch_buffer_append(&hop->raw, chunk, (size_t)count);
113
+ continue;
114
+ }
115
+ break;
116
+ }
117
+ xt_fetch_close_socket(fd);
118
+ }
119
+
120
+ #define XT_FETCH_HOP(c) ((xt_fetch_hop *)(c))
121
+
122
+ /* Main thread: parse the raw response and either follow a redirect (starting a
123
+ * fresh hop) or build the `Response` and settle the promise. */
124
+ static void xt_fetch_hop_complete(xt_async_job *job) {
125
+ xt_fetch_hop *hop = XT_FETCH_HOP(job);
126
+ xt_fetch_state *state = hop->state;
127
+ if (hop->failed) {
128
+ xt_fetch_state_reject(state, "fetch failed");
129
+ return;
130
+ }
131
+
132
+ xt_fetch_buffer *raw = &hop->raw;
133
+ int headEnd = xt_fetch_head_end(raw);
134
+ int status = 0;
135
+ char statusText[256];
136
+ size_t headerStart = 0;
137
+ if (headEnd <= 0 ||
138
+ xt_fetch_parse_status_line(raw->data, raw->length, &status, statusText, sizeof(statusText), &headerStart) != 0) {
139
+ xt_fetch_state_reject(state, "fetch failed: invalid response");
140
+ return;
141
+ }
142
+
143
+ xt_value responseHeaders = xt_headers_new();
144
+ xt_headers_parse_block(responseHeaders, raw->data + headerStart, (size_t)headEnd - 2 - headerStart);
145
+
146
+ int isRedirect = status == 301 || status == 302 || status == 303 || status == 307 || status == 308;
147
+ if (isRedirect && strcmp(state->redirect, "error") == 0) {
148
+ xt_fetch_state_reject(state, "fetch failed: redirect not allowed");
149
+ return;
150
+ }
151
+ if (isRedirect && strcmp(state->redirect, "manual") != 0 && state->redirects < 20) {
152
+ xt_fetch_url parsed;
153
+ if (xt_fetch_parse_url(state->url, &parsed) == 0) {
154
+ const char *location = xt_headers_lookup(responseHeaders, "location");
155
+ char resolved[4096];
156
+ if (location && xt_fetch_resolve_location(&parsed, location, resolved, sizeof(resolved)) == 0) {
157
+ if (status == 303 || ((status == 301 || status == 302) && strcmp(state->method, "POST") == 0)) {
158
+ snprintf(state->method, sizeof(state->method), "GET");
159
+ state->body.length = 0;
160
+ }
161
+ free(state->url);
162
+ size_t length = strlen(resolved);
163
+ state->url = (char *)malloc(length + 1);
164
+ if (!state->url) {
165
+ xt_fetch_state_reject(state, "fetch failed");
166
+ return;
167
+ }
168
+ memcpy(state->url, resolved, length + 1);
169
+ state->redirects++;
170
+ xt_fetch_start(state);
171
+ return;
172
+ }
173
+ }
174
+ }
175
+
176
+ size_t contentLength = 0;
177
+ int hasContentLength = 0;
178
+ int chunked = 0;
179
+ xt_fetch_scan_headers(raw->data, headerStart, (size_t)headEnd - 2, &contentLength, &hasContentLength, &chunked);
180
+ xt_fetch_buffer responseBody;
181
+ xt_fetch_buffer_init(&responseBody);
182
+ if (status == 204 || status == 304 || (status >= 100 && status < 200) || strcmp(state->method, "HEAD") == 0) {
183
+ /* No body by definition. */
184
+ } else if (chunked) {
185
+ xt_fetch_decode_chunked(raw->data, raw->length, (size_t)headEnd, &responseBody);
186
+ } else if (hasContentLength) {
187
+ size_t available = raw->length - (size_t)headEnd;
188
+ size_t take = contentLength < available ? contentLength : available;
189
+ xt_fetch_buffer_append(&responseBody, raw->data + headEnd, take);
190
+ } else {
191
+ xt_fetch_buffer_append(&responseBody, raw->data + headEnd, raw->length - (size_t)headEnd);
192
+ }
193
+
194
+ xt_value response = xt_response_new(responseHeaders, status, statusText, state->url,
195
+ responseBody.data ? responseBody.data : "", responseBody.length);
196
+ xt_fetch_buffer_free(&responseBody);
197
+ xt_value promise = state->promise;
198
+ xt_fetch_state_free(state);
199
+ xt_promise_fulfill(promise, response);
200
+ }
201
+
202
+ /* Main thread: validate the current URL and queue one exchange. */
203
+ static void xt_fetch_start(xt_fetch_state *state) {
204
+ xt_fetch_url parsed;
205
+ if (xt_fetch_parse_url(state->url, &parsed) != 0) {
206
+ xt_fetch_state_reject(state, "Failed to parse URL");
207
+ return;
208
+ }
209
+ if (parsed.secure) {
210
+ xt_fetch_state_reject(state, "fetch failed: https is not supported");
211
+ return;
212
+ }
213
+
214
+ xt_value requestHeaders = xt_headers_from_source(state->headers_source);
215
+ xt_fetch_buffer request;
216
+ xt_fetch_buffer_init(&request);
217
+ xt_fetch_build_request(state->method, &parsed, requestHeaders, state->body.length > 0 ? &state->body : NULL,
218
+ &request);
219
+
220
+ xt_fetch_hop *hop = (xt_fetch_hop *)calloc(1, sizeof(xt_fetch_hop));
221
+ if (!hop) abort();
222
+ hop->base.roots[0] = state->promise;
223
+ hop->base.roots[1] = state->headers_source;
224
+ hop->base.run = xt_fetch_hop_run;
225
+ hop->base.complete = xt_fetch_hop_complete;
226
+ hop->base.release = xt_fetch_hop_release;
227
+ hop->state = state;
228
+ hop->request = request.data;
229
+ hop->request_length = request.length;
230
+ xt_fetch_buffer_init(&hop->raw);
231
+ xt_async_submit(&hop->base);
232
+ }
233
+
234
+ /** Implements the global `fetch(input, init)`. The returned promise settles
235
+ * once a worker has completed the exchange and the reactor dispatches it. */
236
+ xt_value xt_fetch(int32_t argc, xt_value *argv) {
237
+ xt_value input = xt_arg_at(argc, argv, 0);
238
+ xt_value init = xt_arg_at(argc, argv, 1);
239
+ if (input == XT_UNDEFINED || input == XT_NULL) {
240
+ xt_value argument = xt_string_from_cstr("Failed to parse URL from undefined");
241
+ return xt_promise_reject(xt_type_error_ctor(1, &argument));
242
+ }
243
+
244
+ xt_value urlValue = input;
245
+ if (XT_IS_OBJECT(input)) {
246
+ xt_value candidate = xt_object_get_cstr(input, "url");
247
+ if (XT_IS_STRING(candidate)) urlValue = candidate;
248
+ }
249
+ const char *urlText = xt_string_data(xt_to_string(urlValue));
250
+ if (!urlText) {
251
+ xt_value argument = xt_string_from_cstr("Failed to parse URL");
252
+ return xt_promise_reject(xt_type_error_ctor(1, &argument));
253
+ }
254
+
255
+ xt_fetch_state *state = (xt_fetch_state *)calloc(1, sizeof(xt_fetch_state));
256
+ if (!state) abort();
257
+ size_t urlLength = strlen(urlText);
258
+ state->url = (char *)malloc(urlLength + 1);
259
+ if (!state->url) abort();
260
+ memcpy(state->url, urlText, urlLength + 1);
261
+ snprintf(state->method, sizeof(state->method), "GET");
262
+ snprintf(state->redirect, sizeof(state->redirect), "follow");
263
+ xt_fetch_buffer_init(&state->body);
264
+ state->headers_source = XT_UNDEFINED;
265
+ state->redirects = 0;
266
+
267
+ if (XT_IS_OBJECT(init)) {
268
+ xt_value methodValue = xt_object_get_cstr(init, "method");
269
+ if (XT_IS_STRING(methodValue)) {
270
+ const char *text = xt_string_data(methodValue);
271
+ size_t i = 0;
272
+ for (; text && text[i] && i + 1 < sizeof(state->method); i++) {
273
+ char c = text[i];
274
+ state->method[i] = (c >= 'a' && c <= 'z') ? (char)(c - 32) : c;
275
+ }
276
+ state->method[i] = '\0';
277
+ }
278
+ state->headers_source = xt_object_get_cstr(init, "headers");
279
+ xt_value bodySource = xt_object_get_cstr(init, "body");
280
+ if (bodySource != XT_UNDEFINED && bodySource != XT_NULL) {
281
+ xt_value text = xt_to_string(bodySource);
282
+ const char *data = xt_string_data(text);
283
+ if (data) xt_fetch_buffer_append(&state->body, data, (size_t)xt_string_length_value(text));
284
+ }
285
+ xt_value redirectValue = xt_object_get_cstr(init, "redirect");
286
+ if (XT_IS_STRING(redirectValue)) {
287
+ snprintf(state->redirect, sizeof(state->redirect), "%s", xt_string_data(redirectValue));
288
+ }
289
+ }
290
+
291
+ state->promise = xt_promise_create();
292
+ xt_value promise = state->promise;
293
+ xt_fetch_start(state);
294
+ return promise;
295
+ }