@aptre/bldr-saucer 0.2.6 → 0.4.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 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,24 +1,28 @@
1
1
  {
2
2
  "name": "@aptre/bldr-saucer",
3
- "version": "0.2.6",
3
+ "version": "0.4.1",
4
4
  "description": "Native webview bridge for Bldr using Saucer",
5
5
  "main": "index.js",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/aperturerobotics/bldr-saucer.git"
9
- },
6
+ "license": "MIT",
10
7
  "keywords": [
11
8
  "webview",
12
9
  "saucer",
13
10
  "native",
14
11
  "bldr"
15
12
  ],
16
- "author": "Aptre",
17
- "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/aperturerobotics/bldr-saucer.git"
16
+ },
18
17
  "bugs": {
19
18
  "url": "https://github.com/aperturerobotics/bldr-saucer/issues"
20
19
  },
21
20
  "homepage": "https://github.com/aperturerobotics/bldr-saucer#readme",
21
+ "author": {
22
+ "name": "Aperture Robotics LLC.",
23
+ "email": "support@aperture.us",
24
+ "url": "http://aperture.us"
25
+ },
22
26
  "scripts": {
23
27
  "postinstall": "node install.js",
24
28
  "gen": "bun run go:aptre -- generate",
@@ -33,11 +37,11 @@
33
37
  "release:publish": "git push && git push --tags"
34
38
  },
35
39
  "optionalDependencies": {
36
- "@aptre/bldr-saucer-darwin-arm64": "0.2.6",
37
- "@aptre/bldr-saucer-darwin-x64": "0.2.6",
38
- "@aptre/bldr-saucer-linux-x64": "0.2.6",
39
- "@aptre/bldr-saucer-linux-arm64": "0.2.6",
40
- "@aptre/bldr-saucer-win32-x64": "0.2.6"
40
+ "@aptre/bldr-saucer-darwin-arm64": "0.4.1",
41
+ "@aptre/bldr-saucer-darwin-x64": "0.4.1",
42
+ "@aptre/bldr-saucer-linux-x64": "0.4.1",
43
+ "@aptre/bldr-saucer-linux-arm64": "0.4.1",
44
+ "@aptre/bldr-saucer-win32-x64": "0.4.1"
41
45
  },
42
46
  "files": [
43
47
  "index.js",
@@ -46,9 +50,9 @@
46
50
  "CMakeLists.txt"
47
51
  ],
48
52
  "devDependencies": {
49
- "@aptre/common": "^0.30.3"
53
+ "@aptre/common": "^0.32.0"
50
54
  },
51
55
  "dependencies": {
52
- "@aptre/protobuf-es-lite": "^0.5.2"
56
+ "@aptre/protobuf-es-lite": "^1.0.0"
53
57
  }
54
58
  }
package/src/main.cpp CHANGED
@@ -113,16 +113,23 @@ coco::stray start(saucer::application* app) {
113
113
  // Register bldr:// scheme BEFORE creating the webview.
114
114
  saucer::webview::register_scheme("bldr");
115
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
+
116
120
  auto window = saucer::window::create(app).value();
117
- auto webview = saucer::smartview::create({.window = window});
121
+ auto webview = saucer::smartview::create({
122
+ .window = window,
123
+ .storage_path = storage,
124
+ });
118
125
 
119
126
  window->set_title("Bldr");
120
127
  window->set_size({1024, 768});
121
128
 
122
129
  // Handle bldr:// scheme: forward all requests to Go over yamux.
123
- webview->handle_stream_scheme("bldr", [forwarder](saucer::scheme::request req, saucer::scheme::stream_writer writer) {
124
- std::thread([forwarder, req = std::move(req), writer = std::move(writer)]() mutable {
125
- forwarder->forward(req, writer);
130
+ webview->handle_scheme("bldr", [forwarder](saucer::scheme::request req, saucer::scheme::executor executor) {
131
+ std::thread([forwarder, req = std::move(req), executor = std::move(executor)]() mutable {
132
+ forwarder->forward(req, executor);
126
133
  }).detach();
127
134
  });
128
135
 
@@ -157,7 +164,7 @@ coco::stray start(saucer::application* app) {
157
164
  // The smartview's own handler returns unhandled for unrecognized messages,
158
165
  // so this handler sees them next.
159
166
  constexpr std::string_view eval_prefix = "__bldr_eval:";
160
- webview->on<saucer::webview::event::message>({{.func = [eval_registry, eval_prefix](std::string_view message) -> saucer::status {
167
+ webview->on<saucer::webview::event::message>([eval_registry, eval_prefix](std::string_view message) -> saucer::status {
161
168
  if (!message.starts_with(eval_prefix)) {
162
169
  return saucer::status::unhandled;
163
170
  }
@@ -183,7 +190,7 @@ coco::stray start(saucer::application* app) {
183
190
  eval_registry->Deliver(eval_id, "", data);
184
191
  }
185
192
  return saucer::status::handled;
186
- }}});
193
+ });
187
194
 
188
195
  // Start accept loop for Go-initiated streams (debug eval).
189
196
  // webview is a std::expected; use &(*webview) to get a pointer to the contained value.
@@ -14,18 +14,42 @@ static std::string toLower(const std::string& s) {
14
14
  return out;
15
15
  }
16
16
 
17
- // sendError sends an error response to the saucer writer.
18
- static void sendError(saucer::scheme::stream_writer& writer, int status) {
19
- writer.start({.mime = "text/plain", .status = status});
20
- writer.finish();
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
+
26
+ // sendError resolves the executor with an error status response.
27
+ static void sendError(saucer::scheme::executor& executor, int status) {
28
+ executor.resolve({
29
+ .data = saucer::stash::empty(),
30
+ .mime = "text/plain",
31
+ .headers = corsHeaders,
32
+ .status = status,
33
+ });
21
34
  }
22
35
 
23
36
  void SchemeForwarder::forward(const saucer::scheme::request& req,
24
- saucer::scheme::stream_writer& writer) {
37
+ saucer::scheme::executor& executor) {
38
+ // Handle CORS preflight directly without forwarding to Go.
39
+ if (toLower(req.method()) == "options") {
40
+ executor.resolve({
41
+ .data = saucer::stash::empty(),
42
+ .mime = "text/plain",
43
+ .headers = corsHeaders,
44
+ .status = 204,
45
+ });
46
+ return;
47
+ }
48
+
25
49
  // Open a new yamux stream.
26
50
  auto [stream, err] = session_->OpenStream();
27
51
  if (err != yamux::Error::OK || !stream) {
28
- sendError(writer, 502);
52
+ sendError(executor, 502);
29
53
  return;
30
54
  }
31
55
 
@@ -40,62 +64,68 @@ void SchemeForwarder::forward(const saucer::scheme::request& req,
40
64
  }
41
65
 
42
66
  // Check if request has a body.
43
- auto content = req.content();
67
+ auto content = req.content().data();
44
68
  info.has_body = (content.size() > 0);
45
69
 
46
70
  // Serialize and send FetchRequestInfo frame.
47
71
  auto reqInfoMsg = proto::EncodeFetchRequest_Info(info);
48
72
  if (!writeFrame(stream.get(), reqInfoMsg)) {
49
73
  stream->Close();
50
- sendError(writer, 502);
74
+ sendError(executor, 502);
51
75
  return;
52
76
  }
53
77
 
54
78
  // Send body if present.
55
79
  if (info.has_body) {
56
80
  proto::FetchRequestData bodyData;
57
- bodyData.data.assign(
58
- static_cast<const uint8_t*>(content.data()),
59
- static_cast<const uint8_t*>(content.data()) + content.size()
60
- );
81
+ bodyData.data.assign(content.data(), content.data() + content.size());
61
82
  bodyData.done = true;
62
83
 
63
84
  auto reqDataMsg = proto::EncodeFetchRequest_Data(bodyData);
64
85
  if (!writeFrame(stream.get(), reqDataMsg)) {
65
86
  stream->Close();
66
- sendError(writer, 502);
87
+ sendError(executor, 502);
67
88
  return;
68
89
  }
69
90
  }
70
91
 
92
+ // Create streaming stash for incremental response delivery.
93
+ auto result = saucer::scheme::response::stream();
94
+ if (!result) {
95
+ executor.reject(saucer::scheme::error::failed);
96
+ stream->Close();
97
+ return;
98
+ }
99
+ auto [stash, write] = std::move(*result);
100
+
71
101
  // Read response frames from Go.
72
- bool started = false;
102
+ bool resolved = false;
73
103
  bool done = false;
74
104
 
75
105
  while (!done) {
76
106
  std::vector<uint8_t> frame;
77
107
  if (!readFrame(stream.get(), frame)) {
78
- if (!started) {
79
- sendError(writer, 502);
108
+ if (!resolved) {
109
+ executor.reject(saucer::scheme::error::failed);
80
110
  }
81
111
  break;
82
112
  }
83
113
 
84
114
  proto::FetchResponse resp;
85
115
  if (!proto::DecodeFetchResponse(frame.data(), frame.size(), resp)) {
86
- if (!started) {
87
- sendError(writer, 502);
116
+ if (!resolved) {
117
+ executor.reject(saucer::scheme::error::failed);
88
118
  }
89
119
  break;
90
120
  }
91
121
 
92
- // Process ResponseInfo (first frame).
93
- if (resp.has_info && !started) {
94
- started = true;
122
+ // Process ResponseInfo (first frame): resolve executor with headers and streaming stash.
123
+ if (resp.has_info && !resolved) {
124
+ resolved = true;
95
125
 
96
- // Extract Content-Type header (case-insensitive).
126
+ // Extract Content-Type header (case-insensitive) and merge CORS headers.
97
127
  std::string mime = "application/octet-stream";
98
- std::map<std::string, std::string> hdrs;
128
+ std::map<std::string, std::string> hdrs(corsHeaders);
99
129
  for (const auto& [key, val] : resp.info.headers) {
100
130
  if (toLower(key) == "content-type") {
101
131
  mime = val;
@@ -104,22 +134,29 @@ void SchemeForwarder::forward(const saucer::scheme::request& req,
104
134
  }
105
135
  }
106
136
 
107
- writer.start({
137
+ executor.resolve({
138
+ .data = std::move(stash),
108
139
  .mime = mime,
109
140
  .headers = hdrs,
110
141
  .status = static_cast<int>(resp.info.status),
111
142
  });
112
143
  }
113
144
 
114
- // Process ResponseData.
145
+ // Process ResponseData: push body chunks via streaming write callback.
115
146
  if (resp.has_data) {
116
- if (!started) {
117
- started = true;
118
- writer.start({.mime = "application/octet-stream", .status = 200});
147
+ if (!resolved) {
148
+ resolved = true;
149
+ executor.resolve({
150
+ .data = std::move(stash),
151
+ .mime = "application/octet-stream",
152
+ .status = 200,
153
+ });
119
154
  }
120
155
 
121
- if (!resp.data.data.empty() && writer.valid()) {
122
- writer.write(saucer::stash::from(std::move(resp.data.data)));
156
+ if (!resp.data.data.empty()) {
157
+ if (!write({resp.data.data.data(), resp.data.data.size()})) {
158
+ break;
159
+ }
123
160
  }
124
161
 
125
162
  if (resp.data.done) {
@@ -128,9 +165,7 @@ void SchemeForwarder::forward(const saucer::scheme::request& req,
128
165
  }
129
166
  }
130
167
 
131
- if (started) {
132
- writer.finish();
133
- }
168
+ // Destroying write closes the streaming stash.
134
169
  stream->Close();
135
170
  }
136
171
 
@@ -3,6 +3,7 @@
3
3
  #include "fetch_proto.h"
4
4
  #include "yamux/session.hpp"
5
5
 
6
+ #include <saucer/scheme.hpp>
6
7
  #include <saucer/smartview.hpp>
7
8
 
8
9
  #include <cstdint>
@@ -21,7 +22,7 @@ public:
21
22
  explicit SchemeForwarder(yamux::Session* session) : session_(session) {}
22
23
 
23
24
  // forward handles a single scheme request by forwarding it to Go.
24
- void forward(const saucer::scheme::request& req, saucer::scheme::stream_writer& writer);
25
+ void forward(const saucer::scheme::request& req, saucer::scheme::executor& executor);
25
26
 
26
27
  private:
27
28
  // writeFrame writes a length-prefixed frame to a yamux stream.