plusui-native-core 0.1.105 → 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.
Files changed (48) hide show
  1. package/Core/.claude/settings.local.json +7 -0
  2. package/Core/CMakeLists.txt +1 -1
  3. package/Core/Features/API/app-api.ts +28 -28
  4. package/Core/Features/API/browser-api.ts +38 -38
  5. package/Core/Features/API/clipboard-api.ts +21 -21
  6. package/Core/Features/API/display-api.ts +33 -33
  7. package/Core/Features/API/keyboard-api.ts +33 -33
  8. package/Core/Features/API/menu-api.ts +39 -39
  9. package/Core/Features/API/router-api.ts +23 -23
  10. package/Core/Features/API/tray-api.ts +22 -22
  11. package/Core/Features/API/webgpu-api.ts +55 -55
  12. package/Core/Features/App/app.cpp +128 -102
  13. package/Core/Features/Browser/browser.cpp +227 -227
  14. package/Core/Features/Browser/browser.ts +161 -161
  15. package/Core/Features/Clipboard/clipboard.cpp +235 -235
  16. package/Core/Features/Display/display.cpp +212 -212
  17. package/Core/Features/FileDrop/filedrop.cpp +448 -324
  18. package/Core/Features/FileDrop/filedrop.css +421 -421
  19. package/Core/Features/FileDrop/filedrop.ts +0 -7
  20. package/Core/Features/Keyboard/keyboard_linux.cpp +4 -0
  21. package/Core/Features/Router/router.cpp +62 -62
  22. package/Core/Features/Router/router.ts +113 -113
  23. package/Core/Features/Tray/tray.cpp +328 -324
  24. package/Core/Features/WebGPU/webgpu.cpp +948 -948
  25. package/Core/Features/Window/webview.cpp +1009 -1009
  26. package/Core/Features/Window/webview.ts +516 -516
  27. package/Core/Features/Window/window.cpp +2240 -1986
  28. package/Core/include/plusui/api.hpp +237 -237
  29. package/Core/include/plusui/app.hpp +33 -33
  30. package/Core/include/plusui/browser.hpp +67 -67
  31. package/Core/include/plusui/clipboard.hpp +41 -41
  32. package/Core/include/plusui/connect.hpp +340 -340
  33. package/Core/include/plusui/connection.hpp +3 -3
  34. package/Core/include/plusui/display.hpp +90 -90
  35. package/Core/include/plusui/filedrop.hpp +92 -77
  36. package/Core/include/plusui/keyboard.hpp +112 -112
  37. package/Core/include/plusui/menu.hpp +153 -153
  38. package/Core/include/plusui/plusui +18 -18
  39. package/Core/include/plusui/router.hpp +42 -42
  40. package/Core/include/plusui/tray.hpp +94 -94
  41. package/Core/include/plusui/webgpu.hpp +434 -434
  42. package/Core/include/plusui/window.hpp +180 -177
  43. package/Core/scripts/generate-umbrella-header.mjs +77 -77
  44. package/Core/vendor/WebView2EnvironmentOptions.h +406 -406
  45. package/Core/vendor/webview.h +487 -487
  46. package/Core/vendor/webview2.h +52079 -52079
  47. package/README.md +19 -19
  48. 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