@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.
- package/LICENSE +21 -0
- package/NitroJsdom.podspec +63 -0
- package/README.md +95 -0
- package/android/CMakeLists.txt +86 -0
- package/android/build.gradle +148 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +9 -0
- package/android/src/main/java/com/nitrojsdom/NitroJsdomPackage.kt +20 -0
- package/cpp/HybridHtmlSandbox.cpp +171 -0
- package/cpp/HybridHtmlSandbox.hpp +47 -0
- package/cpp/lexbor/LexborDocument.cpp +365 -0
- package/cpp/lexbor/LexborDocument.hpp +71 -0
- package/cpp/quickjs/DOMBindings.cpp +57 -0
- package/cpp/quickjs/DOMBindings.hpp +21 -0
- package/cpp/quickjs/DOMBindingsInternal.cpp +157 -0
- package/cpp/quickjs/DOMBindingsInternal.hpp +83 -0
- package/cpp/quickjs/MutationObservers.cpp +546 -0
- package/cpp/quickjs/MutationObservers.hpp +127 -0
- package/cpp/quickjs/QuickJSRuntime.cpp +475 -0
- package/cpp/quickjs/QuickJSRuntime.hpp +110 -0
- package/cpp/quickjs/Storage.cpp +130 -0
- package/cpp/quickjs/Storage.hpp +40 -0
- package/cpp/quickjs/bindings/ClassListBindings.cpp +241 -0
- package/cpp/quickjs/bindings/ClassListBindings.hpp +29 -0
- package/cpp/quickjs/bindings/DatasetBindings.cpp +120 -0
- package/cpp/quickjs/bindings/DatasetBindings.hpp +20 -0
- package/cpp/quickjs/bindings/DocumentBindings.cpp +155 -0
- package/cpp/quickjs/bindings/DocumentBindings.hpp +19 -0
- package/cpp/quickjs/bindings/ElementBindings.cpp +1106 -0
- package/cpp/quickjs/bindings/ElementBindings.hpp +19 -0
- package/cpp/quickjs/bindings/EventBindings.cpp +352 -0
- package/cpp/quickjs/bindings/EventBindings.hpp +21 -0
- package/cpp/quickjs/bindings/FetchBindings.cpp +347 -0
- package/cpp/quickjs/bindings/FetchBindings.hpp +16 -0
- package/cpp/quickjs/bindings/StyleBindings.cpp +245 -0
- package/cpp/quickjs/bindings/StyleBindings.hpp +22 -0
- package/cpp/quickjs/bindings/TimerBindings.cpp +94 -0
- package/cpp/quickjs/bindings/TimerBindings.hpp +16 -0
- package/cpp/quickjs/bindings/WindowBindings.cpp +211 -0
- package/cpp/quickjs/bindings/WindowBindings.hpp +16 -0
- package/ios/Bridge.h +8 -0
- package/lib/commonjs/classes/JSDOM/JSDOM.class.js +138 -0
- package/lib/commonjs/classes/JSDOM/JSDOM.class.js.map +1 -0
- package/lib/commonjs/classes/JSDOM/index.js +17 -0
- package/lib/commonjs/classes/JSDOM/index.js.map +1 -0
- package/lib/commonjs/classes/JSDOM/types/IFetchOptions.js +2 -0
- package/lib/commonjs/classes/JSDOM/types/IFetchOptions.js.map +1 -0
- package/lib/commonjs/classes/JSDOM/types/IJSDOMOptions.js +6 -0
- package/lib/commonjs/classes/JSDOM/types/IJSDOMOptions.js.map +1 -0
- package/lib/commonjs/classes/JSDOM/types/index.js +2 -0
- package/lib/commonjs/classes/JSDOM/types/index.js.map +1 -0
- package/lib/commonjs/classes/index.js +17 -0
- package/lib/commonjs/classes/index.js.map +1 -0
- package/lib/commonjs/index.js +17 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/HtmlSandbox.nitro.js +6 -0
- package/lib/commonjs/specs/HtmlSandbox.nitro.js.map +1 -0
- package/lib/module/classes/JSDOM/JSDOM.class.js +133 -0
- package/lib/module/classes/JSDOM/JSDOM.class.js.map +1 -0
- package/lib/module/classes/JSDOM/index.js +4 -0
- package/lib/module/classes/JSDOM/index.js.map +1 -0
- package/lib/module/classes/JSDOM/types/IFetchOptions.js +2 -0
- package/lib/module/classes/JSDOM/types/IFetchOptions.js.map +1 -0
- package/lib/module/classes/JSDOM/types/IJSDOMOptions.js +4 -0
- package/lib/module/classes/JSDOM/types/IJSDOMOptions.js.map +1 -0
- package/lib/module/classes/JSDOM/types/index.js +2 -0
- package/lib/module/classes/JSDOM/types/index.js.map +1 -0
- package/lib/module/classes/index.js +4 -0
- package/lib/module/classes/index.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/specs/HtmlSandbox.nitro.js +4 -0
- package/lib/module/specs/HtmlSandbox.nitro.js.map +1 -0
- package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts +74 -0
- package/lib/typescript/src/classes/JSDOM/JSDOM.class.d.ts.map +1 -0
- package/lib/typescript/src/classes/JSDOM/index.d.ts +3 -0
- package/lib/typescript/src/classes/JSDOM/index.d.ts.map +1 -0
- package/lib/typescript/src/classes/JSDOM/types/IFetchOptions.d.ts +14 -0
- package/lib/typescript/src/classes/JSDOM/types/IFetchOptions.d.ts.map +1 -0
- package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts +57 -0
- package/lib/typescript/src/classes/JSDOM/types/IJSDOMOptions.d.ts.map +1 -0
- package/lib/typescript/src/classes/JSDOM/types/index.d.ts +3 -0
- package/lib/typescript/src/classes/JSDOM/types/index.d.ts.map +1 -0
- package/lib/typescript/src/classes/index.d.ts +2 -0
- package/lib/typescript/src/classes/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts +13 -0
- package/lib/typescript/src/specs/HtmlSandbox.nitro.d.ts.map +1 -0
- package/nitro.json +26 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroJsdom+autolinking.cmake +81 -0
- package/nitrogen/generated/android/NitroJsdom+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroJsdomOnLoad.cpp +49 -0
- package/nitrogen/generated/android/NitroJsdomOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrojsdom/NitroJsdomOnLoad.kt +35 -0
- package/nitrogen/generated/ios/NitroJsdom+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroJsdom-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/NitroJsdom-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/NitroJsdom-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/NitroJsdomAutolinking.mm +35 -0
- package/nitrogen/generated/ios/NitroJsdomAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.cpp +26 -0
- package/nitrogen/generated/shared/c++/HybridHtmlSandboxSpec.hpp +73 -0
- package/package.json +131 -0
- package/src/classes/JSDOM/JSDOM.class.ts +145 -0
- package/src/classes/JSDOM/index.ts +2 -0
- package/src/classes/JSDOM/types/IFetchOptions.ts +14 -0
- package/src/classes/JSDOM/types/IJSDOMOptions.ts +57 -0
- package/src/classes/JSDOM/types/index.ts +2 -0
- package/src/classes/index.ts +1 -0
- package/src/index.ts +1 -0
- 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
|