@salve-software/react-native-nitro-jsdom 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/NitroJsdom.podspec +63 -0
  3. package/README.md +95 -0
  4. package/android/CMakeLists.txt +86 -0
  5. package/android/build.gradle +148 -0
  6. package/android/fix-prefab.gradle +51 -0
  7. package/android/gradle.properties +5 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +9 -0
  10. package/android/src/main/java/com/nitrojsdom/NitroJsdomPackage.kt +20 -0
  11. package/cpp/HybridHtmlSandbox.cpp +171 -0
  12. package/cpp/HybridHtmlSandbox.hpp +47 -0
  13. package/cpp/lexbor/LexborDocument.cpp +365 -0
  14. package/cpp/lexbor/LexborDocument.hpp +71 -0
  15. package/cpp/quickjs/DOMBindings.cpp +57 -0
  16. package/cpp/quickjs/DOMBindings.hpp +21 -0
  17. package/cpp/quickjs/DOMBindingsInternal.cpp +157 -0
  18. package/cpp/quickjs/DOMBindingsInternal.hpp +83 -0
  19. package/cpp/quickjs/MutationObservers.cpp +546 -0
  20. package/cpp/quickjs/MutationObservers.hpp +127 -0
  21. package/cpp/quickjs/QuickJSRuntime.cpp +475 -0
  22. package/cpp/quickjs/QuickJSRuntime.hpp +110 -0
  23. package/cpp/quickjs/Storage.cpp +130 -0
  24. package/cpp/quickjs/Storage.hpp +40 -0
  25. package/cpp/quickjs/bindings/ClassListBindings.cpp +241 -0
  26. package/cpp/quickjs/bindings/ClassListBindings.hpp +29 -0
  27. package/cpp/quickjs/bindings/DatasetBindings.cpp +120 -0
  28. package/cpp/quickjs/bindings/DatasetBindings.hpp +20 -0
  29. package/cpp/quickjs/bindings/DocumentBindings.cpp +155 -0
  30. package/cpp/quickjs/bindings/DocumentBindings.hpp +19 -0
  31. package/cpp/quickjs/bindings/ElementBindings.cpp +1106 -0
  32. package/cpp/quickjs/bindings/ElementBindings.hpp +19 -0
  33. package/cpp/quickjs/bindings/EventBindings.cpp +352 -0
  34. package/cpp/quickjs/bindings/EventBindings.hpp +21 -0
  35. package/cpp/quickjs/bindings/FetchBindings.cpp +347 -0
  36. package/cpp/quickjs/bindings/FetchBindings.hpp +16 -0
  37. package/cpp/quickjs/bindings/StyleBindings.cpp +245 -0
  38. package/cpp/quickjs/bindings/StyleBindings.hpp +22 -0
  39. package/cpp/quickjs/bindings/TimerBindings.cpp +94 -0
  40. package/cpp/quickjs/bindings/TimerBindings.hpp +16 -0
  41. package/cpp/quickjs/bindings/WindowBindings.cpp +211 -0
  42. package/cpp/quickjs/bindings/WindowBindings.hpp +16 -0
  43. package/ios/Bridge.h +8 -0
  44. package/lib/commonjs/classes/JSDOM/JSDOM.class.js +138 -0
  45. package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -0
  46. package/lib/commonjs/classes/JSDOM/index.js +17 -0
  47. package/lib/commonjs/classes/JSDOM/index.js.map +1 -0
  48. package/lib/commonjs/classes/JSDOM/types/IFetchOptions.js +2 -0
  49. package/lib/commonjs/classes/JSDOM/types/IFetchOptions.js.map +1 -0
  50. package/lib/commonjs/classes/JSDOM/types/IJSDOMOptions.js +6 -0
  51. package/lib/commonjs/classes/JSDOM/types/IJSDOMOptions.js.map +1 -0
  52. package/lib/commonjs/classes/JSDOM/types/index.js +2 -0
  53. package/lib/commonjs/classes/JSDOM/types/index.js.map +1 -0
  54. package/lib/commonjs/classes/index.js +17 -0
  55. package/lib/commonjs/classes/index.js.map +1 -0
  56. package/lib/commonjs/index.js +17 -0
  57. package/lib/commonjs/index.js.map +1 -0
  58. package/lib/commonjs/package.json +1 -0
  59. package/lib/commonjs/specs/HtmlSandbox.nitro.js +6 -0
  60. package/lib/commonjs/specs/HtmlSandbox.nitro.js.map +1 -0
  61. package/lib/module/classes/JSDOM/JSDOM.class.js +133 -0
  62. package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -0
  63. package/lib/module/classes/JSDOM/index.js +4 -0
  64. package/lib/module/classes/JSDOM/index.js.map +1 -0
  65. package/lib/module/classes/JSDOM/types/IFetchOptions.js +2 -0
  66. package/lib/module/classes/JSDOM/types/IFetchOptions.js.map +1 -0
  67. package/lib/module/classes/JSDOM/types/IJSDOMOptions.js +4 -0
  68. package/lib/module/classes/JSDOM/types/IJSDOMOptions.js.map +1 -0
  69. package/lib/module/classes/JSDOM/types/index.js +2 -0
  70. package/lib/module/classes/JSDOM/types/index.js.map +1 -0
  71. package/lib/module/classes/index.js +4 -0
  72. package/lib/module/classes/index.js.map +1 -0
  73. package/lib/module/index.js +4 -0
  74. package/lib/module/index.js.map +1 -0
  75. package/lib/module/specs/HtmlSandbox.nitro.js +4 -0
  76. package/lib/module/specs/HtmlSandbox.nitro.js.map +1 -0
  77. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts +74 -0
  78. package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -0
  79. package/lib/typescript/src/classes/JSDOM/index.d.ts +3 -0
  80. package/lib/typescript/src/classes/JSDOM/index.d.ts.map +1 -0
  81. package/lib/typescript/src/classes/JSDOM/types/IFetchOptions.d.ts +14 -0
  82. package/lib/typescript/src/classes/JSDOM/types/IFetchOptions.d.ts.map +1 -0
  83. package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts +57 -0
  84. package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts.map +1 -0
  85. package/lib/typescript/src/classes/JSDOM/types/index.d.ts +3 -0
  86. package/lib/typescript/src/classes/JSDOM/types/index.d.ts.map +1 -0
  87. package/lib/typescript/src/classes/index.d.ts +2 -0
  88. package/lib/typescript/src/classes/index.d.ts.map +1 -0
  89. package/lib/typescript/src/index.d.ts +2 -0
  90. package/lib/typescript/src/index.d.ts.map +1 -0
  91. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +13 -0
  92. package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -0
  93. package/nitro.json +26 -0
  94. package/nitrogen/generated/.gitattributes +1 -0
  95. package/nitrogen/generated/android/NitroJsdom+autolinking.cmake +81 -0
  96. package/nitrogen/generated/android/NitroJsdom+autolinking.gradle +27 -0
  97. package/nitrogen/generated/android/NitroJsdomOnLoad.cpp +49 -0
  98. package/nitrogen/generated/android/NitroJsdomOnLoad.hpp +34 -0
  99. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrojsdom/NitroJsdomOnLoad.kt +35 -0
  100. package/nitrogen/generated/ios/NitroJsdom+autolinking.rb +62 -0
  101. package/nitrogen/generated/ios/NitroJsdom-Swift-Cxx-Bridge.cpp +17 -0
  102. package/nitrogen/generated/ios/NitroJsdom-Swift-Cxx-Bridge.hpp +27 -0
  103. package/nitrogen/generated/ios/NitroJsdom-Swift-Cxx-Umbrella.hpp +38 -0
  104. package/nitrogen/generated/ios/NitroJsdomAutolinking.mm +35 -0
  105. package/nitrogen/generated/ios/NitroJsdomAutolinking.swift +16 -0
  106. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.cpp +26 -0
  107. package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +73 -0
  108. package/package.json +131 -0
  109. package/src/classes/JSDOM/JSDOM.class.ts +145 -0
  110. package/src/classes/JSDOM/index.ts +2 -0
  111. package/src/classes/JSDOM/types/IFetchOptions.ts +14 -0
  112. package/src/classes/JSDOM/types/IJSDOMOptions.ts +57 -0
  113. package/src/classes/JSDOM/types/index.ts +2 -0
  114. package/src/classes/index.ts +1 -0
  115. package/src/index.ts +1 -0
  116. package/src/specs/HtmlSandbox.nitro.ts +33 -0
@@ -0,0 +1,475 @@
1
+ #include "QuickJSRuntime.hpp"
2
+ #include "DOMBindings.hpp"
3
+ #include "DOMBindingsInternal.hpp"
4
+ #include "MutationObservers.hpp"
5
+ #include "../lexbor/LexborDocument.hpp"
6
+ #include "quickjs.h"
7
+ #include <stdexcept>
8
+ #include <string>
9
+ #include <mutex>
10
+ #include <thread>
11
+ #include <chrono>
12
+ #include <vector>
13
+
14
+ #ifdef __ANDROID__
15
+ #include <android/log.h>
16
+ #define QJS_LOG(...) __android_log_print(ANDROID_LOG_ERROR, "NitroJsdom", __VA_ARGS__)
17
+ #else
18
+ #define QJS_LOG(...) ((void)0)
19
+ #endif
20
+
21
+ namespace margelo::nitro::nitrojsdom {
22
+
23
+ // ── Helpers ────────────────────────────────────────────────────────────────────
24
+
25
+ static int64_t now_ms() {
26
+ using namespace std::chrono;
27
+ return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
28
+ }
29
+
30
+ // ── Promise rejection tracker ──────────────────────────────────────────────────
31
+
32
+ static void promise_rejection_tracker(JSContext* ctx, JSValue promise,
33
+ JSValue reason, JS_BOOL is_handled,
34
+ void* opaque) {
35
+ (void)promise;
36
+ RuntimeContext* rctx = static_cast<RuntimeContext*>(opaque);
37
+ if (is_handled) {
38
+ if (rctx->pending_rejection) {
39
+ JSValue* stored = static_cast<JSValue*>(rctx->pending_rejection);
40
+ JS_FreeValue(ctx, *stored);
41
+ delete stored;
42
+ rctx->pending_rejection = nullptr;
43
+ }
44
+ return;
45
+ }
46
+ // Free any previously stored rejection
47
+ if (rctx->pending_rejection) {
48
+ JSValue* stored = static_cast<JSValue*>(rctx->pending_rejection);
49
+ JS_FreeValue(ctx, *stored);
50
+ delete stored;
51
+ rctx->pending_rejection = nullptr;
52
+ }
53
+ JSValue* copy = new JSValue(JS_DupValue(ctx, reason));
54
+ rctx->pending_rejection = copy;
55
+ }
56
+
57
+ // ── Constructor / Destructor ───────────────────────────────────────────────────
58
+
59
+ QuickJSRuntime::QuickJSRuntime() = default;
60
+
61
+ QuickJSRuntime::~QuickJSRuntime() {
62
+ // prevent races with drainEventLoop
63
+ std::lock_guard<std::mutex> lock(_mutex);
64
+ if (_context && _ctxState) {
65
+ JSContext* ctx = reinterpret_cast<JSContext*>(_context);
66
+ cleanupTimers();
67
+
68
+ // Free all event listener callbacks
69
+ for (auto& listener : _ctxState->listeners) {
70
+ if (listener.callback) {
71
+ JSValue* cb = static_cast<JSValue*>(listener.callback);
72
+ JS_FreeValue(ctx, *cb);
73
+ delete cb;
74
+ }
75
+ }
76
+ _ctxState->listeners.clear();
77
+
78
+ // Free pending rejection
79
+ if (_ctxState->pending_rejection) {
80
+ JSValue* stored = static_cast<JSValue*>(_ctxState->pending_rejection);
81
+ JS_FreeValue(ctx, *stored);
82
+ delete stored;
83
+ _ctxState->pending_rejection = nullptr;
84
+ }
85
+
86
+ // Free MutationObserver callbacks — must happen before JS_FreeContext
87
+ if (_ctxState->mutation_observers) {
88
+ _ctxState->mutation_observers->clearAll(ctx);
89
+ }
90
+
91
+ // Free node wrapper cache
92
+ clear_node_cache(ctx, _ctxState.get());
93
+ }
94
+
95
+ if (_context) JS_FreeContext(reinterpret_cast<JSContext*>(_context));
96
+ if (_runtime) JS_FreeRuntime(reinterpret_cast<JSRuntime*>(_runtime));
97
+ _context = nullptr;
98
+ _runtime = nullptr;
99
+ }
100
+
101
+ void QuickJSRuntime::cleanupTimers() {
102
+ if (!_context || !_ctxState) return;
103
+ JSContext* ctx = reinterpret_cast<JSContext*>(_context);
104
+
105
+ // drain the heap first — cancelled timers are erased from timer_map but kept
106
+ // in the heap until popped, so the heap is the authoritative cleanup loop.
107
+ while (!_ctxState->timer_heap.empty()) {
108
+ Timer* t = _ctxState->timer_heap.top();
109
+ _ctxState->timer_heap.pop();
110
+ // Only free timers still owned by timer_map (not yet erased by clearTimer)
111
+ if (_ctxState->timer_map.find(t->id) != _ctxState->timer_map.end()) {
112
+ if (t->callback) {
113
+ JSValue* cb = static_cast<JSValue*>(t->callback);
114
+ JS_FreeValue(ctx, *cb);
115
+ delete cb;
116
+ t->callback = nullptr;
117
+ }
118
+ _ctxState->timer_map.erase(t->id);
119
+ delete t;
120
+ } else {
121
+ // Already erased from map (cancelled) — just delete the dangling heap ptr
122
+ delete t;
123
+ }
124
+ }
125
+ // Any remaining map entries not in the heap (should not happen, but be safe)
126
+ for (auto& [id, t] : _ctxState->timer_map) {
127
+ if (t->callback) {
128
+ JSValue* cb = static_cast<JSValue*>(t->callback);
129
+ JS_FreeValue(ctx, *cb);
130
+ delete cb;
131
+ t->callback = nullptr;
132
+ }
133
+ delete t;
134
+ }
135
+ _ctxState->timer_map.clear();
136
+ }
137
+
138
+ // ── initialize ─────────────────────────────────────────────────────────────────
139
+
140
+ void QuickJSRuntime::initialize(const std::string& url) {
141
+ JSRuntime* rt = JS_NewRuntime();
142
+ if (!rt) throw std::runtime_error("QuickJS: failed to create runtime");
143
+ JSContext* ctx = JS_NewContext(rt);
144
+ if (!ctx) {
145
+ JS_FreeRuntime(rt);
146
+ throw std::runtime_error("QuickJS: failed to create context");
147
+ }
148
+ _runtime = rt;
149
+ _context = ctx;
150
+
151
+ // Create RuntimeContext and attach to QuickJS context opaque
152
+ _ctxState = std::make_unique<RuntimeContext>();
153
+ _ctxState->runtime = this;
154
+ _ctxState->mutation_observers = std::make_unique<MutationObservers>();
155
+ JS_SetContextOpaque(ctx, _ctxState.get());
156
+
157
+ // Register promise rejection tracker
158
+ JS_SetHostPromiseRejectionTracker(rt, promise_rejection_tracker, _ctxState.get());
159
+
160
+ JSValue global = JS_GetGlobalObject(ctx);
161
+
162
+ // window = globalThis
163
+ JS_SetPropertyStr(ctx, global, "window", JS_DupValue(ctx, global));
164
+
165
+ // Seed the initial URL — the full window.location object (with protocol,
166
+ // host, pathname, etc.) is built as a JS shim in DOMBindings::install().
167
+ JS_SetPropertyStr(ctx, global, "__initialHref", JS_NewString(ctx, url.c_str()));
168
+
169
+ JS_FreeValue(ctx, global);
170
+ }
171
+
172
+ // ── bindDocument ──────────────────────────────────────────────────────────────
173
+
174
+ void QuickJSRuntime::bindDocument(LexborDocument* document) {
175
+ if (!_context) return;
176
+ _ctxState->document = document;
177
+ DOMBindings::install(this, document);
178
+ }
179
+
180
+ // ── setConsoleCallback ────────────────────────────────────────────────────────
181
+
182
+ void QuickJSRuntime::setConsoleCallback(
183
+ std::function<void(std::string level, std::vector<std::string> args)> cb) {
184
+ if (_ctxState) _ctxState->console_callback = std::move(cb);
185
+ }
186
+
187
+ // ── Dialog callbacks ──────────────────────────────────────────────────────────
188
+
189
+ void QuickJSRuntime::setAlertCallback(std::function<void(const std::string&)> cb) {
190
+ if (_ctxState) _ctxState->alert_callback = std::move(cb);
191
+ }
192
+
193
+ void QuickJSRuntime::setConfirmCallback(std::function<bool(const std::string&)> cb) {
194
+ if (_ctxState) _ctxState->confirm_callback = std::move(cb);
195
+ }
196
+
197
+ void QuickJSRuntime::setPromptCallback(
198
+ std::function<std::optional<std::string>(const std::string&, const std::optional<std::string>&)> cb) {
199
+ if (_ctxState) _ctxState->prompt_callback = std::move(cb);
200
+ }
201
+
202
+ void QuickJSRuntime::setFetchCallback(
203
+ std::function<std::string(const std::string&, const std::string&, const std::string&, const std::optional<std::string>&)> cb) {
204
+ if (_ctxState) _ctxState->fetch_callback = std::move(cb);
205
+ }
206
+
207
+ // ── Event loop drain ──────────────────────────────────────────────────────────
208
+
209
+ void QuickJSRuntime::drainEventLoop() {
210
+ JSRuntime* rt = reinterpret_cast<JSRuntime*>(_runtime);
211
+ JSContext* ctx = reinterpret_cast<JSContext*>(_context);
212
+
213
+ while (true) {
214
+ // 1. Drain all pending microtasks / Promise jobs
215
+ JSContext* job_ctx = nullptr;
216
+ while (true) {
217
+ int r = JS_ExecutePendingJob(rt, &job_ctx);
218
+ if (r == 0) break; // no more jobs
219
+ if (r < 0) {
220
+ // A job threw — capture exception
221
+ JSValue ex = JS_GetException(job_ctx ? job_ctx : ctx);
222
+ std::string err = "Unknown async error";
223
+ JSValue msg_prop = JS_GetPropertyStr(ctx, ex, "message");
224
+ if (!JS_IsException(msg_prop) && !JS_IsUndefined(msg_prop)) {
225
+ const char* s = JS_ToCString(ctx, msg_prop);
226
+ if (s) { err = s; JS_FreeCString(ctx, s); }
227
+ }
228
+ JS_FreeValue(ctx, msg_prop);
229
+ if (err == "Unknown async error") {
230
+ JSValue str_val = JS_ToString(ctx, ex);
231
+ if (!JS_IsException(str_val)) {
232
+ const char* s = JS_ToCString(ctx, str_val);
233
+ if (s) { err = s; JS_FreeCString(ctx, s); }
234
+ }
235
+ JS_FreeValue(ctx, str_val);
236
+ }
237
+ JS_FreeValue(ctx, ex);
238
+ throw std::runtime_error(err);
239
+ }
240
+ }
241
+
242
+ // 2. Check for unhandled rejection set by the tracker
243
+ if (_ctxState->pending_rejection) {
244
+ JSValue* stored = static_cast<JSValue*>(_ctxState->pending_rejection);
245
+ std::string err = "Unhandled Promise rejection";
246
+ JSValue msg_prop = JS_GetPropertyStr(ctx, *stored, "message");
247
+ if (!JS_IsException(msg_prop) && !JS_IsUndefined(msg_prop)) {
248
+ const char* s = JS_ToCString(ctx, msg_prop);
249
+ if (s) { err = s; JS_FreeCString(ctx, s); }
250
+ }
251
+ JS_FreeValue(ctx, msg_prop);
252
+ if (err == "Unhandled Promise rejection") {
253
+ JSValue str_val = JS_ToString(ctx, *stored);
254
+ if (!JS_IsException(str_val)) {
255
+ const char* s = JS_ToCString(ctx, str_val);
256
+ if (s) { err = s; JS_FreeCString(ctx, s); }
257
+ }
258
+ JS_FreeValue(ctx, str_val);
259
+ }
260
+ JS_FreeValue(ctx, *stored);
261
+ delete stored;
262
+ _ctxState->pending_rejection = nullptr;
263
+ throw std::runtime_error(err);
264
+ }
265
+
266
+ // 3. Fire due timers
267
+ int64_t t_now = now_ms();
268
+ bool fired_any = false;
269
+
270
+ while (!_ctxState->timer_heap.empty()) {
271
+ Timer* top = _ctxState->timer_heap.top();
272
+
273
+ // Skip cancelled or already-deleted timers
274
+ if (top->cancelled || _ctxState->timer_map.find(top->id) == _ctxState->timer_map.end()) {
275
+ _ctxState->timer_heap.pop();
276
+ // If cancelled, the timer_map entry was already removed — delete the Timer
277
+ delete top;
278
+ continue;
279
+ }
280
+
281
+ if (top->fire_at_ms > t_now) break; // not yet due
282
+
283
+ _ctxState->timer_heap.pop();
284
+ fired_any = true;
285
+
286
+ if (top->cancelled) {
287
+ delete top;
288
+ continue;
289
+ }
290
+
291
+ try {
292
+ fireTimer(top);
293
+ } catch (...) {
294
+ if (top->callback) {
295
+ JSValue* cb = static_cast<JSValue*>(top->callback);
296
+ JS_FreeValue(ctx, *cb);
297
+ delete cb;
298
+ top->callback = nullptr;
299
+ }
300
+ _ctxState->timer_map.erase(top->id);
301
+ delete top;
302
+ throw;
303
+ }
304
+
305
+ if (top->repeat && !top->cancelled) {
306
+ // Re-arm: keep same interval from original schedule
307
+ int64_t next = top->fire_at_ms + top->interval_ms;
308
+ if (next <= now_ms()) next = now_ms() + top->interval_ms;
309
+ top->fire_at_ms = next;
310
+ _ctxState->timer_heap.push(top);
311
+ } else {
312
+ // Remove from map and free
313
+ _ctxState->timer_map.erase(top->id);
314
+ if (top->callback) {
315
+ JSValue* cb = static_cast<JSValue*>(top->callback);
316
+ JS_FreeValue(ctx, *cb);
317
+ delete cb;
318
+ top->callback = nullptr;
319
+ }
320
+ delete top;
321
+ }
322
+ }
323
+
324
+ // 4. Check if there are more timers waiting
325
+ bool has_active_timers = false;
326
+ {
327
+ // Peek at non-cancelled timers
328
+ auto& heap = _ctxState->timer_heap;
329
+ // We can't peek without modifying. Check timer_map instead.
330
+ for (auto& [id, t] : _ctxState->timer_map) {
331
+ if (!t->cancelled) { has_active_timers = true; break; }
332
+ }
333
+ }
334
+
335
+ if (!has_active_timers && !fired_any) {
336
+ // No timers, no new microtasks — done
337
+ break;
338
+ }
339
+
340
+ if (has_active_timers && !fired_any) {
341
+ // Sleep until next timer fires
342
+ Timer* next_timer = nullptr;
343
+ int64_t min_fire = INT64_MAX;
344
+ for (auto& [id, t] : _ctxState->timer_map) {
345
+ if (!t->cancelled && t->fire_at_ms < min_fire) {
346
+ min_fire = t->fire_at_ms;
347
+ next_timer = t;
348
+ }
349
+ }
350
+ if (next_timer) {
351
+ int64_t sleep_ms = min_fire - now_ms();
352
+ if (sleep_ms > 0) {
353
+ std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
354
+ }
355
+ }
356
+ }
357
+ // else fired_any — loop back to drain microtasks spawned by those callbacks
358
+ }
359
+ }
360
+
361
+ // ── fireTimer ─────────────────────────────────────────────────────────────────
362
+
363
+ void QuickJSRuntime::fireTimer(Timer* t) {
364
+ JSContext* ctx = reinterpret_cast<JSContext*>(_context);
365
+ if (!t->callback) return;
366
+ JSValue* cb = static_cast<JSValue*>(t->callback);
367
+ JSValue ret = JS_Call(ctx, *cb, JS_UNDEFINED, 0, nullptr);
368
+ if (JS_IsException(ret)) {
369
+ JS_FreeValue(ctx, ret);
370
+ // Mark timer as cancelled so it doesn't re-arm
371
+ t->cancelled = true;
372
+ JSValue ex = JS_GetException(ctx);
373
+ std::string err = "Timer callback error";
374
+ JSValue msg_prop = JS_GetPropertyStr(ctx, ex, "message");
375
+ if (!JS_IsException(msg_prop) && !JS_IsUndefined(msg_prop)) {
376
+ const char* s = JS_ToCString(ctx, msg_prop);
377
+ if (s) { err = s; JS_FreeCString(ctx, s); }
378
+ }
379
+ JS_FreeValue(ctx, msg_prop);
380
+ if (err == "Timer callback error") {
381
+ JSValue str_val = JS_ToString(ctx, ex);
382
+ if (!JS_IsException(str_val)) {
383
+ const char* s = JS_ToCString(ctx, str_val);
384
+ if (s) { err = s; JS_FreeCString(ctx, s); }
385
+ }
386
+ JS_FreeValue(ctx, str_val);
387
+ }
388
+ JS_FreeValue(ctx, ex);
389
+ throw std::runtime_error(err);
390
+ }
391
+ JS_FreeValue(ctx, ret);
392
+ }
393
+
394
+ // ── evaluate ──────────────────────────────────────────────────────────────────
395
+
396
+ std::string QuickJSRuntime::evaluate(const std::string& script) {
397
+ JSRuntime* rt = reinterpret_cast<JSRuntime*>(_runtime);
398
+ JSContext* ctx = reinterpret_cast<JSContext*>(_context);
399
+ std::lock_guard<std::mutex> lock(_mutex);
400
+
401
+ // Update the stack reference so QuickJS doesn't false-positive overflow
402
+ // when evaluate() runs on a different thread than initialize().
403
+ JS_UpdateStackTop(rt);
404
+
405
+ JSValue result = JS_Eval(ctx, script.data(), script.size(), "<eval>", JS_EVAL_TYPE_GLOBAL);
406
+
407
+ if (JS_IsException(result)) {
408
+ JS_FreeValue(ctx, result);
409
+ JSValue ex = JS_GetException(ctx);
410
+ std::string err = "Unknown JS error";
411
+
412
+ JSValue msg_prop = JS_GetPropertyStr(ctx, ex, "message");
413
+ if (!JS_IsException(msg_prop) && !JS_IsUndefined(msg_prop)) {
414
+ const char* s = JS_ToCString(ctx, msg_prop);
415
+ if (s) { err = s; JS_FreeCString(ctx, s); }
416
+ }
417
+ JS_FreeValue(ctx, msg_prop);
418
+
419
+ if (err == "Unknown JS error") {
420
+ JSValue str_val = JS_ToString(ctx, ex);
421
+ if (!JS_IsException(str_val)) {
422
+ const char* s = JS_ToCString(ctx, str_val);
423
+ if (s) { err = s; JS_FreeCString(ctx, s); }
424
+ }
425
+ JS_FreeValue(ctx, str_val);
426
+ }
427
+
428
+ JS_FreeValue(ctx, ex);
429
+ QJS_LOG("QuickJS exception: %s", err.c_str());
430
+ throw std::runtime_error(err);
431
+ }
432
+
433
+ // Drain the event loop (microtasks + timers) before resolving the result
434
+ try {
435
+ drainEventLoop();
436
+ } catch (...) {
437
+ JS_FreeValue(ctx, result);
438
+ throw;
439
+ }
440
+
441
+ // If the script returned a Promise, extract its settled value.
442
+ // drainEventLoop() already handled unhandled rejections via pending_rejection,
443
+ // so here we only need to unwrap FULFILLED promises.
444
+ if (JS_IsObject(result)) {
445
+ JSPromiseStateEnum state = JS_PromiseState(ctx, result);
446
+ if (state == JS_PROMISE_FULFILLED) {
447
+ JSValue resolved = JS_PromiseResult(ctx, result);
448
+ JS_FreeValue(ctx, result);
449
+ result = resolved;
450
+ } else if (state == JS_PROMISE_REJECTED) {
451
+ // Fallback: rejection that wasn't caught by pending_rejection tracker
452
+ JSValue reason = JS_PromiseResult(ctx, result);
453
+ JS_FreeValue(ctx, result);
454
+ std::string err = "Promise rejected";
455
+ JSValue msg = JS_GetPropertyStr(ctx, reason, "message");
456
+ if (!JS_IsException(msg) && !JS_IsUndefined(msg)) {
457
+ const char* s = JS_ToCString(ctx, msg);
458
+ if (s) { err = s; JS_FreeCString(ctx, s); }
459
+ }
460
+ JS_FreeValue(ctx, msg);
461
+ JS_FreeValue(ctx, reason);
462
+ throw std::runtime_error(err);
463
+ }
464
+ }
465
+
466
+ JSValue strVal = JS_ToString(ctx, result);
467
+ const char* str = JS_ToCString(ctx, strVal);
468
+ std::string out(str ? str : "");
469
+ JS_FreeCString(ctx, str);
470
+ JS_FreeValue(ctx, strVal);
471
+ JS_FreeValue(ctx, result);
472
+ return out;
473
+ }
474
+
475
+ } // namespace margelo::nitro::nitrojsdom
@@ -0,0 +1,110 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+ #include <mutex>
5
+ #include <functional>
6
+ #include <vector>
7
+ #include <memory>
8
+ #include <queue>
9
+ #include <unordered_map>
10
+ #include <optional>
11
+
12
+ #include "Storage.hpp"
13
+
14
+ namespace margelo::nitro::nitrojsdom {
15
+
16
+ class LexborDocument;
17
+ class MutationObservers;
18
+
19
+ // ── Timer ──────────────────────────────────────────────────────────────────────
20
+
21
+ struct Timer {
22
+ uint32_t id;
23
+ bool repeat; // false = setTimeout, true = setInterval
24
+ int64_t interval_ms;
25
+ int64_t fire_at_ms; // steady_clock epoch ms
26
+ void* callback; // JSValue* (heap-allocated, owned)
27
+ bool cancelled { false };
28
+ };
29
+
30
+ struct TimerCmp {
31
+ bool operator()(const Timer* a, const Timer* b) const {
32
+ return a->fire_at_ms > b->fire_at_ms; // min-heap: smallest fire_at first
33
+ }
34
+ };
35
+
36
+ // ── EventListener ──────────────────────────────────────────────────────────────
37
+
38
+ struct EventListener {
39
+ void* node; // lxb_dom_node_t*
40
+ std::string event_type;
41
+ void* callback; // JSValue* (heap-allocated, owned)
42
+ };
43
+
44
+ // ── RuntimeContext ─────────────────────────────────────────────────────────────
45
+
46
+ struct RuntimeContext {
47
+ LexborDocument* document { nullptr };
48
+ void* runtime { nullptr }; // QuickJSRuntime*
49
+
50
+ uint32_t next_timer_id { 1 };
51
+ std::priority_queue<Timer*, std::vector<Timer*>, TimerCmp> timer_heap;
52
+ std::unordered_map<uint32_t, Timer*> timer_map;
53
+
54
+ std::vector<EventListener> listeners;
55
+ std::function<void(std::string level, std::vector<std::string> args)> console_callback;
56
+
57
+ std::function<void(const std::string& message)> alert_callback;
58
+ std::function<bool(const std::string& message)> confirm_callback;
59
+ std::function<std::optional<std::string>(const std::string& message, const std::optional<std::string>& defaultValue)> prompt_callback;
60
+
61
+ std::function<std::string(const std::string& url, const std::string& method, const std::string& headersJson, const std::optional<std::string>& body)> fetch_callback;
62
+
63
+ void* pending_rejection { nullptr };
64
+ std::unique_ptr<MutationObservers> mutation_observers;
65
+
66
+ Storage local_storage;
67
+ Storage session_storage;
68
+
69
+ // node pointer → heap-allocated JSValue* (DupValue'd strong ref)
70
+ // Ensures the same native node always returns the same JS wrapper object.
71
+ std::unordered_map<void*, void*> node_wrapper_cache;
72
+
73
+ ~RuntimeContext() = default;
74
+ };
75
+
76
+ // ── QuickJSRuntime ─────────────────────────────────────────────────────────────
77
+
78
+ class QuickJSRuntime {
79
+ public:
80
+ QuickJSRuntime();
81
+ ~QuickJSRuntime();
82
+
83
+ void initialize(const std::string& url);
84
+ void bindDocument(LexborDocument* document);
85
+ std::string evaluate(const std::string& script);
86
+
87
+ void setConsoleCallback(std::function<void(std::string level, std::vector<std::string> args)> cb);
88
+
89
+ void setAlertCallback(std::function<void(const std::string&)> cb);
90
+ void setConfirmCallback(std::function<bool(const std::string&)> cb);
91
+ void setPromptCallback(std::function<std::optional<std::string>(const std::string&, const std::optional<std::string>&)> cb);
92
+
93
+ void setFetchCallback(std::function<std::string(const std::string&, const std::string&, const std::string&, const std::optional<std::string>&)> cb);
94
+
95
+ void* context() const { return _context; }
96
+ RuntimeContext* contextState() const { return _ctxState.get(); }
97
+
98
+ private:
99
+ void drainEventLoop();
100
+ void fireTimer(Timer* t);
101
+ void cleanupTimers();
102
+
103
+ void* _runtime { nullptr };
104
+ void* _context { nullptr };
105
+
106
+ std::unique_ptr<RuntimeContext> _ctxState;
107
+ std::mutex _mutex;
108
+ };
109
+
110
+ } // namespace margelo::nitro::nitrojsdom