@aptre/bldr-saucer 0.2.5 → 0.3.1
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/CMakeLists.txt +1 -0
- package/package.json +7 -7
- package/src/fetch_proto.cpp +27 -0
- package/src/fetch_proto.h +17 -0
- package/src/main.cpp +139 -8
- package/src/scheme_forwarder.cpp +19 -3
package/CMakeLists.txt
CHANGED
|
@@ -19,6 +19,7 @@ set(saucer_serializer "Glaze" CACHE STRING "" FORCE)
|
|
|
19
19
|
set(saucer_no_version_check ON CACHE BOOL "" FORCE)
|
|
20
20
|
set(saucer_examples OFF CACHE BOOL "" FORCE)
|
|
21
21
|
set(saucer_tests OFF CACHE BOOL "" FORCE)
|
|
22
|
+
set(saucer_exceptions OFF CACHE BOOL "" FORCE)
|
|
22
23
|
|
|
23
24
|
# cpp-yamux options.
|
|
24
25
|
set(YAMUX_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aptre/bldr-saucer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Native webview bridge for Bldr using Saucer",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"release:publish": "git push && git push --tags"
|
|
34
34
|
},
|
|
35
35
|
"optionalDependencies": {
|
|
36
|
-
"@aptre/bldr-saucer-darwin-arm64": "0.
|
|
37
|
-
"@aptre/bldr-saucer-darwin-x64": "0.
|
|
38
|
-
"@aptre/bldr-saucer-linux-x64": "0.
|
|
39
|
-
"@aptre/bldr-saucer-linux-arm64": "0.
|
|
40
|
-
"@aptre/bldr-saucer-win32-x64": "0.
|
|
36
|
+
"@aptre/bldr-saucer-darwin-arm64": "0.3.1",
|
|
37
|
+
"@aptre/bldr-saucer-darwin-x64": "0.3.1",
|
|
38
|
+
"@aptre/bldr-saucer-linux-x64": "0.3.1",
|
|
39
|
+
"@aptre/bldr-saucer-linux-arm64": "0.3.1",
|
|
40
|
+
"@aptre/bldr-saucer-win32-x64": "0.3.1"
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
43
|
"index.js",
|
|
@@ -49,6 +49,6 @@
|
|
|
49
49
|
"@aptre/common": "^0.30.3"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@aptre/protobuf-es-lite": "^0.
|
|
52
|
+
"@aptre/protobuf-es-lite": "^1.0.0"
|
|
53
53
|
}
|
|
54
54
|
}
|
package/src/fetch_proto.cpp
CHANGED
|
@@ -345,6 +345,33 @@ bool DecodeFetchResponse(const uint8_t* buf, size_t len, FetchResponse& out) {
|
|
|
345
345
|
return true;
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
bool DecodeEvalJSRequest(const uint8_t* buf, size_t len, EvalJSRequest& out) {
|
|
349
|
+
size_t offset = 0;
|
|
350
|
+
while (offset < len) {
|
|
351
|
+
uint32_t field;
|
|
352
|
+
uint8_t wire;
|
|
353
|
+
if (!decodeTag(buf, len, offset, field, wire)) return false;
|
|
354
|
+
switch (field) {
|
|
355
|
+
case 1: { // code
|
|
356
|
+
if (wire != kLengthDelimited) return false;
|
|
357
|
+
if (!decodeString(buf, len, offset, out.code)) return false;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
default:
|
|
361
|
+
if (!skipField(buf, len, offset, wire)) return false;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
std::vector<uint8_t> EncodeEvalJSResponse(const EvalJSResponse& resp) {
|
|
369
|
+
std::vector<uint8_t> buf;
|
|
370
|
+
encodeString(buf, 1, resp.result);
|
|
371
|
+
encodeString(buf, 2, resp.error);
|
|
372
|
+
return buf;
|
|
373
|
+
}
|
|
374
|
+
|
|
348
375
|
bool DecodeSaucerInit(const uint8_t* buf, size_t len, SaucerInit& out) {
|
|
349
376
|
size_t offset = 0;
|
|
350
377
|
while (offset < len) {
|
package/src/fetch_proto.h
CHANGED
|
@@ -57,6 +57,23 @@ struct FetchResponse {
|
|
|
57
57
|
ResponseData data;
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
// EvalJSRequest corresponds to saucer.EvalJSRequest.
|
|
61
|
+
struct EvalJSRequest {
|
|
62
|
+
std::string code; // field 1
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// EvalJSResponse corresponds to saucer.EvalJSResponse.
|
|
66
|
+
struct EvalJSResponse {
|
|
67
|
+
std::string result; // field 1
|
|
68
|
+
std::string error; // field 2
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// DecodeEvalJSRequest decodes an EvalJSRequest protobuf message.
|
|
72
|
+
bool DecodeEvalJSRequest(const uint8_t* buf, size_t len, EvalJSRequest& out);
|
|
73
|
+
|
|
74
|
+
// EncodeEvalJSResponse encodes an EvalJSResponse protobuf message.
|
|
75
|
+
std::vector<uint8_t> EncodeEvalJSResponse(const EvalJSResponse& resp);
|
|
76
|
+
|
|
60
77
|
// EncodeFetchRequest_Info serializes a FetchRequest with request_info (field 1).
|
|
61
78
|
std::vector<uint8_t> EncodeFetchRequest_Info(const FetchRequestInfo& info);
|
|
62
79
|
|
package/src/main.cpp
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
#include "scheme_forwarder.h"
|
|
6
6
|
|
|
7
7
|
#include <atomic>
|
|
8
|
+
#include <condition_variable>
|
|
8
9
|
#include <cstdlib>
|
|
9
10
|
#include <cstring>
|
|
10
11
|
#include <iostream>
|
|
@@ -12,8 +13,64 @@
|
|
|
12
13
|
#include <mutex>
|
|
13
14
|
#include <string>
|
|
14
15
|
#include <thread>
|
|
16
|
+
#include <unordered_map>
|
|
15
17
|
#include <vector>
|
|
16
18
|
|
|
19
|
+
// EvalRegistry tracks pending eval requests and their results.
|
|
20
|
+
// Worker threads register a request ID, execute JS that posts results via
|
|
21
|
+
// the saucer message channel, then wait on a condition variable for the
|
|
22
|
+
// message handler to deliver the result.
|
|
23
|
+
struct EvalRegistry {
|
|
24
|
+
struct Pending {
|
|
25
|
+
bool ready = false;
|
|
26
|
+
std::string result;
|
|
27
|
+
std::string error;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
std::mutex mtx;
|
|
31
|
+
std::condition_variable cv;
|
|
32
|
+
std::unordered_map<std::string, Pending> pending;
|
|
33
|
+
|
|
34
|
+
// Register registers a new eval request and returns the ID.
|
|
35
|
+
void Register(const std::string& id) {
|
|
36
|
+
std::lock_guard<std::mutex> lock(mtx);
|
|
37
|
+
pending[id] = Pending{};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Deliver delivers a result for a pending eval request.
|
|
41
|
+
// Returns true if the ID was found.
|
|
42
|
+
bool Deliver(const std::string& id, const std::string& result, const std::string& error) {
|
|
43
|
+
std::lock_guard<std::mutex> lock(mtx);
|
|
44
|
+
auto it = pending.find(id);
|
|
45
|
+
if (it == pending.end()) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
it->second.ready = true;
|
|
49
|
+
it->second.result = result;
|
|
50
|
+
it->second.error = error;
|
|
51
|
+
cv.notify_all();
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Wait waits for a result for the given eval ID (up to timeout_ms).
|
|
56
|
+
// Returns the response, with empty fields on timeout.
|
|
57
|
+
bldr::proto::EvalJSResponse Wait(const std::string& id, int timeout_ms) {
|
|
58
|
+
std::unique_lock<std::mutex> lock(mtx);
|
|
59
|
+
cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), [this, &id] {
|
|
60
|
+
auto it = pending.find(id);
|
|
61
|
+
return it != pending.end() && it->second.ready;
|
|
62
|
+
});
|
|
63
|
+
bldr::proto::EvalJSResponse resp;
|
|
64
|
+
auto it = pending.find(id);
|
|
65
|
+
if (it != pending.end()) {
|
|
66
|
+
resp.result = std::move(it->second.result);
|
|
67
|
+
resp.error = std::move(it->second.error);
|
|
68
|
+
pending.erase(it);
|
|
69
|
+
}
|
|
70
|
+
return resp;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
17
74
|
coco::stray start(saucer::application* app) {
|
|
18
75
|
const char* runtime_id_env = std::getenv("BLDR_RUNTIME_ID");
|
|
19
76
|
if (!runtime_id_env) {
|
|
@@ -56,8 +113,16 @@ coco::stray start(saucer::application* app) {
|
|
|
56
113
|
// Register bldr:// scheme BEFORE creating the webview.
|
|
57
114
|
saucer::webview::register_scheme("bldr");
|
|
58
115
|
|
|
116
|
+
// Construct a per-instance storage path from the runtime ID.
|
|
117
|
+
// This ensures concurrent bldr-saucer instances don't share webview storage.
|
|
118
|
+
auto storage = std::filesystem::temp_directory_path() / ("bldr-saucer-" + runtime_id);
|
|
119
|
+
|
|
59
120
|
auto window = saucer::window::create(app).value();
|
|
60
|
-
auto webview = saucer::smartview::create({
|
|
121
|
+
auto webview = saucer::smartview::create({
|
|
122
|
+
.window = window,
|
|
123
|
+
.non_persistent_data_store = true,
|
|
124
|
+
.storage_path = storage,
|
|
125
|
+
});
|
|
61
126
|
|
|
62
127
|
window->set_title("Bldr");
|
|
63
128
|
window->set_size({1024, 768});
|
|
@@ -90,10 +155,49 @@ coco::stray start(saucer::application* app) {
|
|
|
90
155
|
auto webview_mtx = std::make_shared<std::mutex>();
|
|
91
156
|
auto webview_alive = std::make_shared<std::atomic<bool>>(true);
|
|
92
157
|
|
|
158
|
+
// Eval result registry: worker threads register pending evals, the message
|
|
159
|
+
// handler delivers results from JavaScript back to the waiting thread.
|
|
160
|
+
auto eval_registry = std::make_shared<EvalRegistry>();
|
|
161
|
+
|
|
162
|
+
// Register a message handler to intercept eval results from JavaScript.
|
|
163
|
+
// The Go side wraps JS code so it posts the result via postMessage with a
|
|
164
|
+
// prefix format: __bldr_eval:<eval_id>:r:<result> or __bldr_eval:<eval_id>:e:<error>.
|
|
165
|
+
// The smartview's own handler returns unhandled for unrecognized messages,
|
|
166
|
+
// so this handler sees them next.
|
|
167
|
+
constexpr std::string_view eval_prefix = "__bldr_eval:";
|
|
168
|
+
webview->on<saucer::webview::event::message>([eval_registry, eval_prefix](std::string_view message) -> saucer::status {
|
|
169
|
+
if (!message.starts_with(eval_prefix)) {
|
|
170
|
+
return saucer::status::unhandled;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Parse prefix format: __bldr_eval:<eval_id>:<type>:<data>
|
|
174
|
+
auto rest = message.substr(eval_prefix.size());
|
|
175
|
+
auto sep1 = rest.find(':');
|
|
176
|
+
if (sep1 == std::string_view::npos || sep1 + 2 >= rest.size()) {
|
|
177
|
+
return saucer::status::unhandled;
|
|
178
|
+
}
|
|
179
|
+
auto sep2 = rest.find(':', sep1 + 1);
|
|
180
|
+
if (sep2 == std::string_view::npos) {
|
|
181
|
+
return saucer::status::unhandled;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
std::string eval_id(rest.substr(0, sep1));
|
|
185
|
+
char type = rest[sep1 + 1];
|
|
186
|
+
std::string data(rest.substr(sep2 + 1));
|
|
187
|
+
|
|
188
|
+
if (type == 'r') {
|
|
189
|
+
eval_registry->Deliver(eval_id, data, "");
|
|
190
|
+
} else {
|
|
191
|
+
eval_registry->Deliver(eval_id, "", data);
|
|
192
|
+
}
|
|
193
|
+
return saucer::status::handled;
|
|
194
|
+
});
|
|
195
|
+
|
|
93
196
|
// Start accept loop for Go-initiated streams (debug eval).
|
|
94
197
|
// webview is a std::expected; use &(*webview) to get a pointer to the contained value.
|
|
95
198
|
auto* webview_ptr = &(*webview);
|
|
96
|
-
|
|
199
|
+
auto eval_counter = std::make_shared<std::atomic<uint64_t>>(0);
|
|
200
|
+
std::thread accept_thread([session, webview_ptr, webview_mtx, webview_alive, eval_registry, eval_counter]() {
|
|
97
201
|
while (true) {
|
|
98
202
|
auto [stream, err] = session->Accept();
|
|
99
203
|
if (err != yamux::Error::OK || !stream) {
|
|
@@ -101,7 +205,7 @@ coco::stray start(saucer::application* app) {
|
|
|
101
205
|
}
|
|
102
206
|
|
|
103
207
|
// Handle each stream in a detached thread so accept loop continues.
|
|
104
|
-
std::thread([stream, webview_ptr, webview_mtx, webview_alive]() {
|
|
208
|
+
std::thread([stream, webview_ptr, webview_mtx, webview_alive, eval_registry, eval_counter]() {
|
|
105
209
|
// Read length-prefixed command frame.
|
|
106
210
|
uint8_t len_buf[4];
|
|
107
211
|
size_t total = 0;
|
|
@@ -132,7 +236,28 @@ coco::stray start(saucer::application* app) {
|
|
|
132
236
|
return;
|
|
133
237
|
}
|
|
134
238
|
|
|
135
|
-
|
|
239
|
+
// Decode the EvalJSRequest protobuf from Go.
|
|
240
|
+
bldr::proto::EvalJSRequest req;
|
|
241
|
+
if (!bldr::proto::DecodeEvalJSRequest(data.data(), data.size(), req)) {
|
|
242
|
+
stream->Close();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The code from Go is already wrapped in an async IIFE that posts
|
|
247
|
+
// the result via postMessage. It contains a placeholder __EVAL_ID__
|
|
248
|
+
// that we replace with a unique ID for result correlation.
|
|
249
|
+
std::string code = std::move(req.code);
|
|
250
|
+
std::string eval_id = "e" + std::to_string(eval_counter->fetch_add(1));
|
|
251
|
+
|
|
252
|
+
// Replace the __EVAL_ID__ placeholder with the actual eval ID.
|
|
253
|
+
const std::string placeholder = "__EVAL_ID__";
|
|
254
|
+
auto pos = code.find(placeholder);
|
|
255
|
+
if (pos != std::string::npos) {
|
|
256
|
+
code.replace(pos, placeholder.size(), eval_id);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Register the eval request before executing the code.
|
|
260
|
+
eval_registry->Register(eval_id);
|
|
136
261
|
|
|
137
262
|
// Execute the JavaScript code in the webview (guarded against shutdown).
|
|
138
263
|
// Cast to webview* to call webview::execute(cstring_view) instead of
|
|
@@ -145,13 +270,19 @@ coco::stray start(saucer::application* app) {
|
|
|
145
270
|
}
|
|
146
271
|
}
|
|
147
272
|
|
|
148
|
-
//
|
|
149
|
-
|
|
273
|
+
// Wait for the JavaScript result (30 second timeout).
|
|
274
|
+
auto resp = eval_registry->Wait(eval_id, 30000);
|
|
275
|
+
if (resp.result.empty() && resp.error.empty()) {
|
|
276
|
+
resp.error = "eval timeout";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Encode the EvalJSResponse protobuf and send it back.
|
|
280
|
+
auto resp_buf = bldr::proto::EncodeEvalJSResponse(resp);
|
|
150
281
|
uint8_t resp_len_buf[4];
|
|
151
|
-
uint32_t resp_len = static_cast<uint32_t>(
|
|
282
|
+
uint32_t resp_len = static_cast<uint32_t>(resp_buf.size());
|
|
152
283
|
std::memcpy(resp_len_buf, &resp_len, 4);
|
|
153
284
|
stream->Write(resp_len_buf, 4);
|
|
154
|
-
stream->Write(
|
|
285
|
+
stream->Write(resp_buf.data(), resp_buf.size());
|
|
155
286
|
stream->Close();
|
|
156
287
|
}).detach();
|
|
157
288
|
}
|
package/src/scheme_forwarder.cpp
CHANGED
|
@@ -14,14 +14,30 @@ static std::string toLower(const std::string& s) {
|
|
|
14
14
|
return out;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// corsHeaders are Access-Control headers added to all scheme responses.
|
|
18
|
+
// WebKit treats custom scheme origins as opaque (null), so all fetch requests
|
|
19
|
+
// from pages loaded via bldr:// are cross-origin. These headers allow them.
|
|
20
|
+
static const std::map<std::string, std::string> corsHeaders = {
|
|
21
|
+
{"Access-Control-Allow-Origin", "*"},
|
|
22
|
+
{"Access-Control-Allow-Methods", "GET, POST, OPTIONS"},
|
|
23
|
+
{"Access-Control-Allow-Headers", "*"},
|
|
24
|
+
};
|
|
25
|
+
|
|
17
26
|
// sendError sends an error response to the saucer writer.
|
|
18
27
|
static void sendError(saucer::scheme::stream_writer& writer, int status) {
|
|
19
|
-
writer.start({.mime = "text/plain", .status = status});
|
|
28
|
+
writer.start({.mime = "text/plain", .headers = corsHeaders, .status = status});
|
|
20
29
|
writer.finish();
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
void SchemeForwarder::forward(const saucer::scheme::request& req,
|
|
24
33
|
saucer::scheme::stream_writer& writer) {
|
|
34
|
+
// Handle CORS preflight directly without forwarding to Go.
|
|
35
|
+
if (toLower(req.method()) == "options") {
|
|
36
|
+
writer.start({.mime = "text/plain", .headers = corsHeaders, .status = 204});
|
|
37
|
+
writer.finish();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
// Open a new yamux stream.
|
|
26
42
|
auto [stream, err] = session_->OpenStream();
|
|
27
43
|
if (err != yamux::Error::OK || !stream) {
|
|
@@ -93,9 +109,9 @@ void SchemeForwarder::forward(const saucer::scheme::request& req,
|
|
|
93
109
|
if (resp.has_info && !started) {
|
|
94
110
|
started = true;
|
|
95
111
|
|
|
96
|
-
// Extract Content-Type header (case-insensitive).
|
|
112
|
+
// Extract Content-Type header (case-insensitive) and merge CORS headers.
|
|
97
113
|
std::string mime = "application/octet-stream";
|
|
98
|
-
std::map<std::string, std::string> hdrs;
|
|
114
|
+
std::map<std::string, std::string> hdrs(corsHeaders);
|
|
99
115
|
for (const auto& [key, val] : resp.info.headers) {
|
|
100
116
|
if (toLower(key) == "content-type") {
|
|
101
117
|
mime = val;
|