plusui-native-core 0.1.104 → 0.1.106
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/Core/.claude/settings.local.json +7 -0
- package/Core/CMakeLists.txt +1 -1
- package/Core/Features/API/Connect_API.ts +20 -52
- package/Core/Features/API/app-api.ts +28 -28
- package/Core/Features/API/browser-api.ts +38 -38
- package/Core/Features/API/clipboard-api.ts +21 -21
- package/Core/Features/API/display-api.ts +33 -33
- package/Core/Features/API/keyboard-api.ts +33 -33
- package/Core/Features/API/menu-api.ts +39 -39
- package/Core/Features/API/router-api.ts +23 -23
- package/Core/Features/API/tray-api.ts +22 -22
- package/Core/Features/API/webgpu-api.ts +55 -55
- package/Core/Features/App/app.cpp +128 -102
- package/Core/Features/Browser/browser.cpp +227 -227
- package/Core/Features/Browser/browser.ts +161 -161
- package/Core/Features/Clipboard/clipboard.cpp +235 -235
- package/Core/Features/Display/display.cpp +212 -212
- package/Core/Features/FileDrop/filedrop.cpp +448 -324
- package/Core/Features/FileDrop/filedrop.css +421 -421
- package/Core/Features/FileDrop/filedrop.ts +5 -9
- package/Core/Features/Keyboard/keyboard_linux.cpp +4 -0
- package/Core/Features/Router/router.cpp +62 -62
- package/Core/Features/Router/router.ts +113 -113
- package/Core/Features/Tray/tray.cpp +328 -324
- package/Core/Features/WebGPU/webgpu.cpp +948 -948
- package/Core/Features/Window/webview.cpp +1026 -1014
- package/Core/Features/Window/webview.ts +516 -516
- package/Core/Features/Window/window.cpp +2265 -1988
- package/Core/include/plusui/api.hpp +237 -237
- package/Core/include/plusui/app.hpp +33 -33
- package/Core/include/plusui/browser.hpp +67 -67
- package/Core/include/plusui/clipboard.hpp +41 -41
- package/Core/include/plusui/connect.hpp +340 -340
- package/Core/include/plusui/connection.hpp +3 -3
- package/Core/include/plusui/display.hpp +90 -90
- package/Core/include/plusui/filedrop.hpp +92 -77
- package/Core/include/plusui/keyboard.hpp +112 -112
- package/Core/include/plusui/menu.hpp +153 -153
- package/Core/include/plusui/plusui +18 -18
- package/Core/include/plusui/router.hpp +42 -42
- package/Core/include/plusui/tray.hpp +94 -94
- package/Core/include/plusui/webgpu.hpp +434 -434
- package/Core/include/plusui/window.hpp +180 -177
- package/Core/scripts/generate-umbrella-header.mjs +77 -77
- package/Core/vendor/WebView2EnvironmentOptions.h +406 -406
- package/Core/vendor/webview.h +487 -487
- package/Core/vendor/webview2.h +52079 -52079
- package/README.md +19 -19
- package/package.json +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#pragma once
|
|
2
|
-
|
|
3
|
-
#include <functional>
|
|
4
|
-
#include <map>
|
|
5
|
-
#include <nlohmann/json.hpp>
|
|
6
|
-
#include <string>
|
|
7
|
-
#include <vector>
|
|
8
|
-
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <functional>
|
|
4
|
+
#include <map>
|
|
5
|
+
#include <nlohmann/json.hpp>
|
|
6
|
+
#include <string>
|
|
7
|
+
#include <vector>
|
|
8
|
+
|
|
9
9
|
// ============================================================================
|
|
10
10
|
// PlusUI Connect
|
|
11
11
|
// ============================================================================
|
|
@@ -69,335 +69,335 @@
|
|
|
69
69
|
// connect::settings.onSetTheme = [](json) { ... }; connect::settings.themeChange({{...}});
|
|
70
70
|
//
|
|
71
71
|
// ============================================================================
|
|
72
|
-
|
|
73
|
-
namespace plusui {
|
|
74
|
-
|
|
75
|
-
class Window;
|
|
76
|
-
|
|
77
|
-
class Connect {
|
|
78
|
-
public:
|
|
79
|
-
using EventHandler = std::function<void(const nlohmann::json &)>;
|
|
80
|
-
using CallHandler =
|
|
81
|
-
std::function<nlohmann::json(const nlohmann::json &)>;
|
|
82
|
-
using SubscriptionHandler =
|
|
83
|
-
std::function<void(bool subscribed, const nlohmann::json &)>;
|
|
84
|
-
|
|
85
|
-
class Feature {
|
|
86
|
-
Connect *owner;
|
|
87
|
-
std::string scope;
|
|
88
|
-
|
|
89
|
-
std::string scopedName(const std::string &name) const {
|
|
90
|
-
const std::string prefix = scope + ".";
|
|
91
|
-
if (name.rfind(prefix, 0) == 0) {
|
|
92
|
-
return name;
|
|
93
|
-
}
|
|
94
|
-
return prefix + name;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
public:
|
|
98
|
-
Feature(Connect *parent, std::string featureScope)
|
|
99
|
-
: owner(parent), scope(std::move(featureScope)) {}
|
|
100
|
-
|
|
101
|
-
void on(const std::string &name, EventHandler handler) {
|
|
102
|
-
owner->on(scopedName(name), std::move(handler));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
void emit(const std::string &name, const nlohmann::json &payload) {
|
|
106
|
-
owner->emit(scopedName(name), payload);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
void onCall(const std::string &name, CallHandler handler) {
|
|
110
|
-
owner->onCall(scopedName(name), std::move(handler));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
void onSubscription(const std::string &name,
|
|
114
|
-
SubscriptionHandler handler) {
|
|
115
|
-
owner->onSubscription(scopedName(name), std::move(handler));
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
// ============================================================
|
|
120
|
-
// Channel — single-name channel object (legacy / backward compat)
|
|
121
|
-
//
|
|
122
|
-
// Mirror of the TypeScript createChannel() object:
|
|
123
|
-
// download.on([](const json& p) { ... });
|
|
124
|
-
// download.emit({{"progress", 50}});
|
|
125
|
-
//
|
|
126
|
-
// Obtained via connect.channel("download") or declared in
|
|
127
|
-
// the auto-generated connections.gen.hpp.
|
|
128
|
-
// ============================================================
|
|
129
|
-
class Channel {
|
|
130
|
-
Connect *owner;
|
|
131
|
-
std::string name_;
|
|
132
|
-
|
|
133
|
-
public:
|
|
134
|
-
Channel() : owner(nullptr) {}
|
|
135
|
-
Channel(Connect *parent, std::string channelName)
|
|
136
|
-
: owner(parent), name_(std::move(channelName)) {}
|
|
137
|
-
|
|
138
|
-
// Register a listener — mirrors TypeScript: download.on((d) => { ... })
|
|
139
|
-
void on(EventHandler handler) {
|
|
140
|
-
if (owner) owner->on(name_, std::move(handler));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Send a message — mirrors TypeScript: download.emit({ key: value })
|
|
144
|
-
void emit(const nlohmann::json &payload = nlohmann::json::object()) {
|
|
145
|
-
if (owner) owner->emit(name_, payload);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const std::string &name() const { return name_; }
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
private:
|
|
152
|
-
std::function<void(const std::string &)> outboundHandler;
|
|
153
|
-
std::map<std::string, std::vector<EventHandler>> eventHandlers;
|
|
154
|
-
std::map<std::string, CallHandler> callHandlers;
|
|
155
|
-
std::map<std::string, SubscriptionHandler> subscriptionHandlers;
|
|
156
|
-
|
|
157
|
-
void emitEnvelope(const std::string &kind, const std::string &name,
|
|
158
|
-
const nlohmann::json &payload,
|
|
159
|
-
const std::string &id = std::string(),
|
|
160
|
-
const std::string &error = std::string()) {
|
|
161
|
-
if (!outboundHandler) {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
nlohmann::json j;
|
|
166
|
-
j["kind"] = kind;
|
|
167
|
-
j["name"] = name;
|
|
168
|
-
if (!id.empty()) {
|
|
169
|
-
j["id"] = id;
|
|
170
|
-
}
|
|
171
|
-
j["payload"] = payload;
|
|
172
|
-
if (!error.empty()) {
|
|
173
|
-
j["error"] = error;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
outboundHandler(j.dump());
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
public:
|
|
180
|
-
|
|
181
|
-
virtual ~Connect() = default;
|
|
182
|
-
|
|
183
|
-
// Mirror frontend API: register message listener by name
|
|
184
|
-
void on(const std::string &name, EventHandler handler) {
|
|
185
|
-
eventHandlers[name].push_back(std::move(handler));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
Feature feature(const std::string &scope) { return Feature(this, scope); }
|
|
189
|
-
|
|
190
|
-
// Create a named channel object — mirrors TypeScript createChannel('name')
|
|
191
|
-
Channel channel(const std::string &name) { return Channel(this, name); }
|
|
192
|
-
|
|
193
|
-
// Request/response helper for call primitive
|
|
194
|
-
void onCall(const std::string &name, CallHandler handler) {
|
|
195
|
-
callHandlers[name] = std::move(handler);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Subscription helper for stream/query primitives
|
|
199
|
-
void onSubscription(const std::string &name, SubscriptionHandler handler) {
|
|
200
|
-
subscriptionHandlers[name] = std::move(handler);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Setup the outbound message handler
|
|
204
|
-
void setOutbound(std::function<void(const std::string &)> h) {
|
|
205
|
-
outboundHandler = std::move(h);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
void setOutboundHandler(std::function<void(const std::string &)> h) {
|
|
209
|
-
setOutbound(std::move(h));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* emit() - Send a message to the frontend
|
|
214
|
-
*
|
|
215
|
-
* Works for all 5 communication patterns:
|
|
216
|
-
* - SIMPLEX: emit("notification", {{"msg", "Hi!"}})
|
|
217
|
-
* - CALL RESPONSE: emitResult(id, name, payload)
|
|
218
|
-
* - STREAM/PUB-SUB: emit("updates", {{"value", x}}) // call repeatedly
|
|
219
|
-
* - STATE: emit("theme", {{"mode", "dark"}})
|
|
220
|
-
*/
|
|
221
|
-
void emit(const std::string &name, const nlohmann::json &payload) {
|
|
222
|
-
emitEnvelope("event", name, payload);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
void emitResult(const std::string &id, const std::string &name,
|
|
226
|
-
const nlohmann::json &payload) {
|
|
227
|
-
emitEnvelope("result", name, payload, id);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
void emitError(const std::string &id, const std::string &name,
|
|
231
|
-
const std::string &message) {
|
|
232
|
-
emitEnvelope("error", name, nlohmann::json::object(), id, message);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* call() - Make a call to the frontend and wait for response
|
|
237
|
-
*
|
|
238
|
-
* Enables bidirectional request/response:
|
|
239
|
-
* Backend: auto answer = connect.call("ui.promptUser", {{"msg", "Sure?"}});
|
|
240
|
-
* Frontend: connect.ui.handlePromptUser = async (data) => { ... return result; };
|
|
241
|
-
*/
|
|
242
|
-
void call(const std::string &name, const nlohmann::json &payload,
|
|
243
|
-
std::function<void(const nlohmann::json &)> onResult = nullptr,
|
|
244
|
-
std::function<void(const std::string &)> onError = nullptr) {
|
|
245
|
-
auto id = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
|
|
246
|
-
|
|
247
|
-
if (onResult || onError) {
|
|
248
|
-
// Store pending callback for when frontend responds
|
|
249
|
-
on(name + ".__result__." + id, [this, id, name, onResult, onError](const nlohmann::json &p) {
|
|
250
|
-
if (p.contains("error") && onError) {
|
|
251
|
-
onError(p["error"].get<std::string>());
|
|
252
|
-
} else if (onResult) {
|
|
253
|
-
onResult(p.value("payload", nlohmann::json::object()));
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
emitEnvelope("call", name, payload, id);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Parse and dispatch incoming messages from frontend
|
|
262
|
-
void dispatchMessage(const std::string &message) {
|
|
263
|
-
try {
|
|
264
|
-
auto j = nlohmann::json::parse(message);
|
|
265
|
-
nlohmann::json envelope = j;
|
|
266
|
-
|
|
267
|
-
if (j.is_object() && j.value("method", std::string()) ==
|
|
268
|
-
"connection.dispatch") {
|
|
269
|
-
auto paramsIt = j.find("params");
|
|
270
|
-
if (paramsIt != j.end() && paramsIt->is_array() &&
|
|
271
|
-
!paramsIt->empty() && (*paramsIt)[0].is_object()) {
|
|
272
|
-
envelope = (*paramsIt)[0];
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
std::string name = envelope.value("name", "");
|
|
277
|
-
nlohmann::json payload =
|
|
278
|
-
envelope.value("payload", nlohmann::json::object());
|
|
279
|
-
std::string kind = envelope.value("kind", "fire");
|
|
280
|
-
std::string id = envelope.value("id", "");
|
|
281
|
-
|
|
282
|
-
if (name.empty()) {
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (kind == "call") {
|
|
287
|
-
try {
|
|
288
|
-
nlohmann::json responsePayload = nlohmann::json::object();
|
|
289
|
-
auto callIt = callHandlers.find(name);
|
|
290
|
-
if (callIt != callHandlers.end()) {
|
|
291
|
-
responsePayload = callIt->second(payload);
|
|
292
|
-
} else {
|
|
293
|
-
responsePayload = handleCall(name, payload);
|
|
294
|
-
}
|
|
295
|
-
if (!id.empty()) {
|
|
296
|
-
emitResult(id, name, responsePayload);
|
|
297
|
-
}
|
|
298
|
-
} catch (const std::exception &e) {
|
|
299
|
-
if (!id.empty()) {
|
|
300
|
-
emitError(id, name, e.what());
|
|
301
|
-
}
|
|
302
|
-
} catch (...) {
|
|
303
|
-
if (!id.empty()) {
|
|
304
|
-
emitError(id, name, "Unknown error");
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (kind == "result" || kind == "error") {
|
|
311
|
-
// Response from a frontend call — dispatch to pending listeners
|
|
312
|
-
std::string resultKey = name + ".__result__." + id;
|
|
313
|
-
auto listenersIt = eventHandlers.find(resultKey);
|
|
314
|
-
if (listenersIt != eventHandlers.end()) {
|
|
315
|
-
nlohmann::json resultPayload;
|
|
316
|
-
if (kind == "error") {
|
|
317
|
-
resultPayload["error"] = envelope.value("error", "Unknown error");
|
|
318
|
-
} else {
|
|
319
|
-
resultPayload["payload"] = payload;
|
|
320
|
-
}
|
|
321
|
-
for (const auto &listener : listenersIt->second) {
|
|
322
|
-
listener(resultPayload);
|
|
323
|
-
}
|
|
324
|
-
eventHandlers.erase(resultKey);
|
|
325
|
-
}
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (kind == "sub") {
|
|
330
|
-
auto subIt = subscriptionHandlers.find(name);
|
|
331
|
-
if (subIt != subscriptionHandlers.end()) {
|
|
332
|
-
subIt->second(true, payload);
|
|
333
|
-
}
|
|
334
|
-
handleSubscription(name, true, payload);
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (kind == "unsub") {
|
|
339
|
-
auto subIt = subscriptionHandlers.find(name);
|
|
340
|
-
if (subIt != subscriptionHandlers.end()) {
|
|
341
|
-
subIt->second(false, payload);
|
|
342
|
-
}
|
|
343
|
-
handleSubscription(name, false, payload);
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
auto listenersIt = eventHandlers.find(name);
|
|
348
|
-
if (listenersIt != eventHandlers.end()) {
|
|
349
|
-
for (const auto &listener : listenersIt->second) {
|
|
350
|
-
listener(payload);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
handleMessage(name, payload);
|
|
355
|
-
} catch (...) {
|
|
356
|
-
// Silently ignore malformed messages
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
void dispatch(const std::string &message) { dispatchMessage(message); }
|
|
361
|
-
|
|
362
|
-
protected:
|
|
363
|
-
virtual nlohmann::json handleCall(const std::string &name,
|
|
364
|
-
const nlohmann::json &payload) {
|
|
365
|
-
handleMessage(name, payload);
|
|
366
|
-
return nlohmann::json::object();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
virtual void handleSubscription(const std::string &name, bool subscribed,
|
|
370
|
-
const nlohmann::json &payload) {
|
|
371
|
-
(void)name;
|
|
372
|
-
(void)subscribed;
|
|
373
|
-
(void)payload;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* handleMessage() - Receive messages from the frontend
|
|
378
|
-
*
|
|
379
|
-
* Override this to handle incoming messages. This is your "on()" equivalent.
|
|
380
|
-
*
|
|
381
|
-
* For the new semantic API, prefer using the generated connect:: namespace
|
|
382
|
-
* instead of overriding this method:
|
|
383
|
-
*
|
|
384
|
-
* connect::user.handleFetch = [](const json& p) -> json {
|
|
385
|
-
* return database.getUser(p["id"]);
|
|
386
|
-
* };
|
|
387
|
-
*
|
|
388
|
-
* This method remains for backward compatibility and catch-all handling.
|
|
389
|
-
*/
|
|
390
|
-
virtual void handleMessage(const std::string &name,
|
|
391
|
-
const nlohmann::json &payload) {
|
|
392
|
-
(void)name;
|
|
393
|
-
(void)payload;
|
|
394
|
-
}
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
void bindConnect(Window &window, Connect &connect);
|
|
398
|
-
|
|
399
|
-
// Legacy aliases for backwards compatibility
|
|
400
|
-
using Bridge = Connect;
|
|
401
|
-
using Connection = Connect;
|
|
402
|
-
|
|
403
|
-
} // namespace plusui
|
|
72
|
+
|
|
73
|
+
namespace plusui {
|
|
74
|
+
|
|
75
|
+
class Window;
|
|
76
|
+
|
|
77
|
+
class Connect {
|
|
78
|
+
public:
|
|
79
|
+
using EventHandler = std::function<void(const nlohmann::json &)>;
|
|
80
|
+
using CallHandler =
|
|
81
|
+
std::function<nlohmann::json(const nlohmann::json &)>;
|
|
82
|
+
using SubscriptionHandler =
|
|
83
|
+
std::function<void(bool subscribed, const nlohmann::json &)>;
|
|
84
|
+
|
|
85
|
+
class Feature {
|
|
86
|
+
Connect *owner;
|
|
87
|
+
std::string scope;
|
|
88
|
+
|
|
89
|
+
std::string scopedName(const std::string &name) const {
|
|
90
|
+
const std::string prefix = scope + ".";
|
|
91
|
+
if (name.rfind(prefix, 0) == 0) {
|
|
92
|
+
return name;
|
|
93
|
+
}
|
|
94
|
+
return prefix + name;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public:
|
|
98
|
+
Feature(Connect *parent, std::string featureScope)
|
|
99
|
+
: owner(parent), scope(std::move(featureScope)) {}
|
|
100
|
+
|
|
101
|
+
void on(const std::string &name, EventHandler handler) {
|
|
102
|
+
owner->on(scopedName(name), std::move(handler));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
void emit(const std::string &name, const nlohmann::json &payload) {
|
|
106
|
+
owner->emit(scopedName(name), payload);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
void onCall(const std::string &name, CallHandler handler) {
|
|
110
|
+
owner->onCall(scopedName(name), std::move(handler));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
void onSubscription(const std::string &name,
|
|
114
|
+
SubscriptionHandler handler) {
|
|
115
|
+
owner->onSubscription(scopedName(name), std::move(handler));
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// ============================================================
|
|
120
|
+
// Channel — single-name channel object (legacy / backward compat)
|
|
121
|
+
//
|
|
122
|
+
// Mirror of the TypeScript createChannel() object:
|
|
123
|
+
// download.on([](const json& p) { ... });
|
|
124
|
+
// download.emit({{"progress", 50}});
|
|
125
|
+
//
|
|
126
|
+
// Obtained via connect.channel("download") or declared in
|
|
127
|
+
// the auto-generated connections.gen.hpp.
|
|
128
|
+
// ============================================================
|
|
129
|
+
class Channel {
|
|
130
|
+
Connect *owner;
|
|
131
|
+
std::string name_;
|
|
132
|
+
|
|
133
|
+
public:
|
|
134
|
+
Channel() : owner(nullptr) {}
|
|
135
|
+
Channel(Connect *parent, std::string channelName)
|
|
136
|
+
: owner(parent), name_(std::move(channelName)) {}
|
|
137
|
+
|
|
138
|
+
// Register a listener — mirrors TypeScript: download.on((d) => { ... })
|
|
139
|
+
void on(EventHandler handler) {
|
|
140
|
+
if (owner) owner->on(name_, std::move(handler));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Send a message — mirrors TypeScript: download.emit({ key: value })
|
|
144
|
+
void emit(const nlohmann::json &payload = nlohmann::json::object()) {
|
|
145
|
+
if (owner) owner->emit(name_, payload);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const std::string &name() const { return name_; }
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
private:
|
|
152
|
+
std::function<void(const std::string &)> outboundHandler;
|
|
153
|
+
std::map<std::string, std::vector<EventHandler>> eventHandlers;
|
|
154
|
+
std::map<std::string, CallHandler> callHandlers;
|
|
155
|
+
std::map<std::string, SubscriptionHandler> subscriptionHandlers;
|
|
156
|
+
|
|
157
|
+
void emitEnvelope(const std::string &kind, const std::string &name,
|
|
158
|
+
const nlohmann::json &payload,
|
|
159
|
+
const std::string &id = std::string(),
|
|
160
|
+
const std::string &error = std::string()) {
|
|
161
|
+
if (!outboundHandler) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
nlohmann::json j;
|
|
166
|
+
j["kind"] = kind;
|
|
167
|
+
j["name"] = name;
|
|
168
|
+
if (!id.empty()) {
|
|
169
|
+
j["id"] = id;
|
|
170
|
+
}
|
|
171
|
+
j["payload"] = payload;
|
|
172
|
+
if (!error.empty()) {
|
|
173
|
+
j["error"] = error;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
outboundHandler(j.dump());
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public:
|
|
180
|
+
|
|
181
|
+
virtual ~Connect() = default;
|
|
182
|
+
|
|
183
|
+
// Mirror frontend API: register message listener by name
|
|
184
|
+
void on(const std::string &name, EventHandler handler) {
|
|
185
|
+
eventHandlers[name].push_back(std::move(handler));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
Feature feature(const std::string &scope) { return Feature(this, scope); }
|
|
189
|
+
|
|
190
|
+
// Create a named channel object — mirrors TypeScript createChannel('name')
|
|
191
|
+
Channel channel(const std::string &name) { return Channel(this, name); }
|
|
192
|
+
|
|
193
|
+
// Request/response helper for call primitive
|
|
194
|
+
void onCall(const std::string &name, CallHandler handler) {
|
|
195
|
+
callHandlers[name] = std::move(handler);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Subscription helper for stream/query primitives
|
|
199
|
+
void onSubscription(const std::string &name, SubscriptionHandler handler) {
|
|
200
|
+
subscriptionHandlers[name] = std::move(handler);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Setup the outbound message handler
|
|
204
|
+
void setOutbound(std::function<void(const std::string &)> h) {
|
|
205
|
+
outboundHandler = std::move(h);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
void setOutboundHandler(std::function<void(const std::string &)> h) {
|
|
209
|
+
setOutbound(std::move(h));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* emit() - Send a message to the frontend
|
|
214
|
+
*
|
|
215
|
+
* Works for all 5 communication patterns:
|
|
216
|
+
* - SIMPLEX: emit("notification", {{"msg", "Hi!"}})
|
|
217
|
+
* - CALL RESPONSE: emitResult(id, name, payload)
|
|
218
|
+
* - STREAM/PUB-SUB: emit("updates", {{"value", x}}) // call repeatedly
|
|
219
|
+
* - STATE: emit("theme", {{"mode", "dark"}})
|
|
220
|
+
*/
|
|
221
|
+
void emit(const std::string &name, const nlohmann::json &payload) {
|
|
222
|
+
emitEnvelope("event", name, payload);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
void emitResult(const std::string &id, const std::string &name,
|
|
226
|
+
const nlohmann::json &payload) {
|
|
227
|
+
emitEnvelope("result", name, payload, id);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
void emitError(const std::string &id, const std::string &name,
|
|
231
|
+
const std::string &message) {
|
|
232
|
+
emitEnvelope("error", name, nlohmann::json::object(), id, message);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* call() - Make a call to the frontend and wait for response
|
|
237
|
+
*
|
|
238
|
+
* Enables bidirectional request/response:
|
|
239
|
+
* Backend: auto answer = connect.call("ui.promptUser", {{"msg", "Sure?"}});
|
|
240
|
+
* Frontend: connect.ui.handlePromptUser = async (data) => { ... return result; };
|
|
241
|
+
*/
|
|
242
|
+
void call(const std::string &name, const nlohmann::json &payload,
|
|
243
|
+
std::function<void(const nlohmann::json &)> onResult = nullptr,
|
|
244
|
+
std::function<void(const std::string &)> onError = nullptr) {
|
|
245
|
+
auto id = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
|
|
246
|
+
|
|
247
|
+
if (onResult || onError) {
|
|
248
|
+
// Store pending callback for when frontend responds
|
|
249
|
+
on(name + ".__result__." + id, [this, id, name, onResult, onError](const nlohmann::json &p) {
|
|
250
|
+
if (p.contains("error") && onError) {
|
|
251
|
+
onError(p["error"].get<std::string>());
|
|
252
|
+
} else if (onResult) {
|
|
253
|
+
onResult(p.value("payload", nlohmann::json::object()));
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
emitEnvelope("call", name, payload, id);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Parse and dispatch incoming messages from frontend
|
|
262
|
+
void dispatchMessage(const std::string &message) {
|
|
263
|
+
try {
|
|
264
|
+
auto j = nlohmann::json::parse(message);
|
|
265
|
+
nlohmann::json envelope = j;
|
|
266
|
+
|
|
267
|
+
if (j.is_object() && j.value("method", std::string()) ==
|
|
268
|
+
"connection.dispatch") {
|
|
269
|
+
auto paramsIt = j.find("params");
|
|
270
|
+
if (paramsIt != j.end() && paramsIt->is_array() &&
|
|
271
|
+
!paramsIt->empty() && (*paramsIt)[0].is_object()) {
|
|
272
|
+
envelope = (*paramsIt)[0];
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
std::string name = envelope.value("name", "");
|
|
277
|
+
nlohmann::json payload =
|
|
278
|
+
envelope.value("payload", nlohmann::json::object());
|
|
279
|
+
std::string kind = envelope.value("kind", "fire");
|
|
280
|
+
std::string id = envelope.value("id", "");
|
|
281
|
+
|
|
282
|
+
if (name.empty()) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (kind == "call") {
|
|
287
|
+
try {
|
|
288
|
+
nlohmann::json responsePayload = nlohmann::json::object();
|
|
289
|
+
auto callIt = callHandlers.find(name);
|
|
290
|
+
if (callIt != callHandlers.end()) {
|
|
291
|
+
responsePayload = callIt->second(payload);
|
|
292
|
+
} else {
|
|
293
|
+
responsePayload = handleCall(name, payload);
|
|
294
|
+
}
|
|
295
|
+
if (!id.empty()) {
|
|
296
|
+
emitResult(id, name, responsePayload);
|
|
297
|
+
}
|
|
298
|
+
} catch (const std::exception &e) {
|
|
299
|
+
if (!id.empty()) {
|
|
300
|
+
emitError(id, name, e.what());
|
|
301
|
+
}
|
|
302
|
+
} catch (...) {
|
|
303
|
+
if (!id.empty()) {
|
|
304
|
+
emitError(id, name, "Unknown error");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (kind == "result" || kind == "error") {
|
|
311
|
+
// Response from a frontend call — dispatch to pending listeners
|
|
312
|
+
std::string resultKey = name + ".__result__." + id;
|
|
313
|
+
auto listenersIt = eventHandlers.find(resultKey);
|
|
314
|
+
if (listenersIt != eventHandlers.end()) {
|
|
315
|
+
nlohmann::json resultPayload;
|
|
316
|
+
if (kind == "error") {
|
|
317
|
+
resultPayload["error"] = envelope.value("error", "Unknown error");
|
|
318
|
+
} else {
|
|
319
|
+
resultPayload["payload"] = payload;
|
|
320
|
+
}
|
|
321
|
+
for (const auto &listener : listenersIt->second) {
|
|
322
|
+
listener(resultPayload);
|
|
323
|
+
}
|
|
324
|
+
eventHandlers.erase(resultKey);
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (kind == "sub") {
|
|
330
|
+
auto subIt = subscriptionHandlers.find(name);
|
|
331
|
+
if (subIt != subscriptionHandlers.end()) {
|
|
332
|
+
subIt->second(true, payload);
|
|
333
|
+
}
|
|
334
|
+
handleSubscription(name, true, payload);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (kind == "unsub") {
|
|
339
|
+
auto subIt = subscriptionHandlers.find(name);
|
|
340
|
+
if (subIt != subscriptionHandlers.end()) {
|
|
341
|
+
subIt->second(false, payload);
|
|
342
|
+
}
|
|
343
|
+
handleSubscription(name, false, payload);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
auto listenersIt = eventHandlers.find(name);
|
|
348
|
+
if (listenersIt != eventHandlers.end()) {
|
|
349
|
+
for (const auto &listener : listenersIt->second) {
|
|
350
|
+
listener(payload);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
handleMessage(name, payload);
|
|
355
|
+
} catch (...) {
|
|
356
|
+
// Silently ignore malformed messages
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
void dispatch(const std::string &message) { dispatchMessage(message); }
|
|
361
|
+
|
|
362
|
+
protected:
|
|
363
|
+
virtual nlohmann::json handleCall(const std::string &name,
|
|
364
|
+
const nlohmann::json &payload) {
|
|
365
|
+
handleMessage(name, payload);
|
|
366
|
+
return nlohmann::json::object();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
virtual void handleSubscription(const std::string &name, bool subscribed,
|
|
370
|
+
const nlohmann::json &payload) {
|
|
371
|
+
(void)name;
|
|
372
|
+
(void)subscribed;
|
|
373
|
+
(void)payload;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* handleMessage() - Receive messages from the frontend
|
|
378
|
+
*
|
|
379
|
+
* Override this to handle incoming messages. This is your "on()" equivalent.
|
|
380
|
+
*
|
|
381
|
+
* For the new semantic API, prefer using the generated connect:: namespace
|
|
382
|
+
* instead of overriding this method:
|
|
383
|
+
*
|
|
384
|
+
* connect::user.handleFetch = [](const json& p) -> json {
|
|
385
|
+
* return database.getUser(p["id"]);
|
|
386
|
+
* };
|
|
387
|
+
*
|
|
388
|
+
* This method remains for backward compatibility and catch-all handling.
|
|
389
|
+
*/
|
|
390
|
+
virtual void handleMessage(const std::string &name,
|
|
391
|
+
const nlohmann::json &payload) {
|
|
392
|
+
(void)name;
|
|
393
|
+
(void)payload;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
void bindConnect(Window &window, Connect &connect);
|
|
398
|
+
|
|
399
|
+
// Legacy aliases for backwards compatibility
|
|
400
|
+
using Bridge = Connect;
|
|
401
|
+
using Connection = Connect;
|
|
402
|
+
|
|
403
|
+
} // namespace plusui
|