@lingxia/bridge 0.5.0 → 0.6.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.
@@ -0,0 +1,1597 @@
1
+ import { BRIDGE_ERROR } from "./types";
2
+ import { toBridgeError, toNativeError } from "./invocation";
3
+ import { installNativeComponentCoverageMonitor } from "./nativecomponents/coverage-monitor";
4
+ import { BRIDGE_CONFIG, getCommunicationMethod, getPlatformOS, isAndroid, isHarmony, isIOS, isMacOS, isDesktop, } from "./runtime-env";
5
+ const NATIVE_HANDLER_NAME = "LingXia";
6
+ const GLOBAL_RECEIVER_NAME = "__LingXiaRecvMessage";
7
+ const DEFAULT_TIMEOUT_MS = 5000;
8
+ const HANDSHAKE_TIMEOUT_MS = 10000;
9
+ const HANDSHAKE_MAX_RETRIES = 3;
10
+ const LOG_PREFIX = "[LX.Bridge]";
11
+ const MESSAGE_PORT_TYPE = "messageport";
12
+ const JS_INTERFACE_TYPE = "jsinterface";
13
+ const OUTBOX_LIMIT = 256;
14
+ const APPLE_DOWNSTREAM_URL = "lx-apple://bridge/downstream";
15
+ const APPLE_RECONNECT_BASE_MS = 200;
16
+ const APPLE_RECONNECT_MAX_MS = 2000;
17
+ const debugFlags = { data: false, proto: false, all: false };
18
+ const earlyNativeMessages = [];
19
+ function installEarlyReceiver() {
20
+ if (typeof window === "undefined")
21
+ return;
22
+ if (typeof window[GLOBAL_RECEIVER_NAME] === "function")
23
+ return;
24
+ window[GLOBAL_RECEIVER_NAME] = (message) => {
25
+ earlyNativeMessages.push(message);
26
+ };
27
+ }
28
+ function activateReceiver(receiver) {
29
+ if (typeof window === "undefined")
30
+ return;
31
+ window[GLOBAL_RECEIVER_NAME] = receiver;
32
+ if (earlyNativeMessages.length === 0)
33
+ return;
34
+ const queued = earlyNativeMessages.splice(0, earlyNativeMessages.length);
35
+ for (const message of queued) {
36
+ try {
37
+ receiver(message);
38
+ }
39
+ catch (err) {
40
+ warn("Failed to replay queued native message:", err);
41
+ }
42
+ }
43
+ }
44
+ function isDebugEnabled(flag) {
45
+ return debugFlags.all || debugFlags[flag];
46
+ }
47
+ function log(...args) {
48
+ console.log(LOG_PREFIX, ...args);
49
+ }
50
+ function warn(...args) {
51
+ console.warn(LOG_PREFIX, ...args);
52
+ }
53
+ function error(...args) {
54
+ console.error(LOG_PREFIX, ...args);
55
+ }
56
+ function safeStringify(obj, space) {
57
+ const seen = new WeakSet();
58
+ return JSON.stringify(obj, (_key, value) => {
59
+ if (typeof value === "object" && value !== null) {
60
+ if (seen.has(value))
61
+ return "[Circular]";
62
+ seen.add(value);
63
+ }
64
+ return value;
65
+ }, space);
66
+ }
67
+ function stringifyForNative(obj) {
68
+ try {
69
+ return JSON.stringify(obj);
70
+ }
71
+ catch {
72
+ return safeStringify(obj);
73
+ }
74
+ }
75
+ function deepCopy(data) {
76
+ try {
77
+ if (typeof structuredClone === "function")
78
+ return structuredClone(data);
79
+ return JSON.parse(JSON.stringify(data));
80
+ }
81
+ catch {
82
+ return {};
83
+ }
84
+ }
85
+ const communicationMethod = getCommunicationMethod();
86
+ installEarlyReceiver();
87
+ // Transport
88
+ let messagePort = null;
89
+ let appleDownstreamConnected = false;
90
+ let appleDownstreamTask = null;
91
+ let appleDownstreamAbortController = null;
92
+ let appleReconnectTimer = null;
93
+ let appleReconnectDelayMs = APPLE_RECONNECT_BASE_MS;
94
+ const portInitState = {
95
+ listenerInstalled: false,
96
+ promise: null,
97
+ resolve: null,
98
+ reject: null,
99
+ timer: null,
100
+ };
101
+ const portInitMessageListener = {
102
+ handleEvent: (event) => {
103
+ const messageEvent = event;
104
+ if (messageEvent.data !== "LingXia-port-init")
105
+ return;
106
+ const port = messageEvent.ports?.[0];
107
+ if (!port)
108
+ return;
109
+ LingXiaBridge._connectWebMessagePort(port);
110
+ portInitState.resolve?.(port);
111
+ },
112
+ };
113
+ function installMessagePortInitListener() {
114
+ if (portInitState.listenerInstalled)
115
+ return;
116
+ portInitState.listenerInstalled = true;
117
+ window.addEventListener("message", portInitMessageListener);
118
+ }
119
+ function cleanupPortInit() {
120
+ if (portInitState.timer !== null) {
121
+ window.clearTimeout(portInitState.timer);
122
+ portInitState.timer = null;
123
+ }
124
+ portInitState.resolve = null;
125
+ portInitState.reject = null;
126
+ portInitState.promise = null;
127
+ }
128
+ function useAppleDownstreamTransport() {
129
+ return communicationMethod === "webkit" && (isIOS() || isMacOS());
130
+ }
131
+ function clearAppleReconnectTimer() {
132
+ if (appleReconnectTimer !== null) {
133
+ clearTimeout(appleReconnectTimer);
134
+ appleReconnectTimer = null;
135
+ }
136
+ }
137
+ function closeActiveChannelsFromTransport(reason) {
138
+ for (const [id, channel] of Array.from(activeChannels.entries())) {
139
+ activeChannels.delete(id);
140
+ channel._emitClose(BRIDGE_ERROR.STREAM_CLOSED, reason);
141
+ }
142
+ }
143
+ function rejectAllPendingForTransport(reason) {
144
+ for (const id of Array.from(pendingReq.keys())) {
145
+ rejectPendingRequest(id, {
146
+ code: BRIDGE_ERROR.STREAM_CLOSED,
147
+ message: reason,
148
+ });
149
+ }
150
+ for (const id of Array.from(pendingChannels.keys())) {
151
+ rejectPendingChannel(id, {
152
+ code: BRIDGE_ERROR.STREAM_CLOSED,
153
+ message: reason,
154
+ });
155
+ }
156
+ closeActiveChannelsFromTransport(reason);
157
+ }
158
+ function resetHandshakeState(reason, rejectPending) {
159
+ clearHandshakeTimer();
160
+ handshakeSessionId = null;
161
+ handshakeDone = false;
162
+ helloSent = false;
163
+ handshakeRetryCount = 0;
164
+ if (rejectPending)
165
+ rejectAllPendingForTransport(reason);
166
+ }
167
+ function processAppleDownstreamBuffer(buffer) {
168
+ let remaining = buffer;
169
+ while (true) {
170
+ const newlineIndex = remaining.indexOf("\n");
171
+ if (newlineIndex < 0)
172
+ break;
173
+ const line = remaining.slice(0, newlineIndex).trim();
174
+ remaining = remaining.slice(newlineIndex + 1);
175
+ if (!line)
176
+ continue;
177
+ try {
178
+ handleIncomingMessage(JSON.parse(line));
179
+ }
180
+ catch (e) {
181
+ warn("Apple downstream parse error:", e, line);
182
+ }
183
+ }
184
+ return remaining;
185
+ }
186
+ async function runAppleDownstream() {
187
+ const controller = new AbortController();
188
+ appleDownstreamAbortController = controller;
189
+ let response;
190
+ try {
191
+ response = await fetch(APPLE_DOWNSTREAM_URL, {
192
+ method: "GET",
193
+ cache: "no-store",
194
+ headers: { Accept: "application/x-ndjson" },
195
+ signal: controller.signal,
196
+ });
197
+ }
198
+ catch (e) {
199
+ if (controller.signal.aborted)
200
+ return;
201
+ throw e;
202
+ }
203
+ if (!response.ok) {
204
+ throw new Error(`Apple downstream HTTP ${response.status}`);
205
+ }
206
+ if (!response.body) {
207
+ throw new Error("Apple downstream response body unavailable");
208
+ }
209
+ appleDownstreamConnected = true;
210
+ appleReconnectDelayMs = APPLE_RECONNECT_BASE_MS;
211
+ if (isDebugEnabled("proto"))
212
+ log("Apple downstream connected");
213
+ startHandshake();
214
+ const reader = response.body.getReader();
215
+ const decoder = new TextDecoder();
216
+ let buffered = "";
217
+ try {
218
+ while (true) {
219
+ const { value, done } = await reader.read();
220
+ if (done)
221
+ break;
222
+ buffered += decoder.decode(value, { stream: true });
223
+ buffered = processAppleDownstreamBuffer(buffered);
224
+ }
225
+ buffered += decoder.decode();
226
+ const tail = buffered.trim();
227
+ if (tail) {
228
+ try {
229
+ handleIncomingMessage(JSON.parse(tail));
230
+ }
231
+ catch (e) {
232
+ warn("Apple downstream trailing parse error:", e, tail);
233
+ }
234
+ }
235
+ }
236
+ finally {
237
+ try {
238
+ reader.releaseLock();
239
+ }
240
+ catch { }
241
+ }
242
+ }
243
+ function scheduleAppleDownstreamReconnect(reason) {
244
+ if (!useAppleDownstreamTransport())
245
+ return;
246
+ clearAppleReconnectTimer();
247
+ const delay = appleReconnectDelayMs;
248
+ appleReconnectDelayMs = Math.min(appleReconnectDelayMs * 2, APPLE_RECONNECT_MAX_MS);
249
+ const message = `Apple downstream disconnected, retrying in ${delay}ms: ${reason}`;
250
+ if (delay <= APPLE_RECONNECT_BASE_MS) {
251
+ log(message);
252
+ }
253
+ else {
254
+ warn(message);
255
+ }
256
+ appleReconnectTimer = setTimeout(() => {
257
+ appleReconnectTimer = null;
258
+ ensureAppleDownstream();
259
+ }, delay);
260
+ }
261
+ function ensureAppleDownstream() {
262
+ if (!useAppleDownstreamTransport())
263
+ return;
264
+ if (appleDownstreamTask)
265
+ return;
266
+ clearAppleReconnectTimer();
267
+ appleDownstreamTask = runAppleDownstream()
268
+ .catch((e) => {
269
+ if (appleDownstreamAbortController?.signal.aborted)
270
+ return;
271
+ warn("Apple downstream failed:", e);
272
+ })
273
+ .finally(() => {
274
+ const aborted = appleDownstreamAbortController?.signal.aborted ?? false;
275
+ appleDownstreamTask = null;
276
+ appleDownstreamAbortController = null;
277
+ const wasConnected = appleDownstreamConnected;
278
+ appleDownstreamConnected = false;
279
+ resetHandshakeState("Apple downstream closed", wasConnected);
280
+ if (!aborted)
281
+ scheduleAppleDownstreamReconnect("stream closed");
282
+ });
283
+ }
284
+ function getMessagePort() {
285
+ if (messagePort)
286
+ return Promise.resolve(messagePort);
287
+ if (portInitState.promise)
288
+ return portInitState.promise;
289
+ const timeoutMs = 5000;
290
+ installMessagePortInitListener();
291
+ portInitState.promise = new Promise((resolve, reject) => {
292
+ portInitState.resolve = (port) => {
293
+ cleanupPortInit();
294
+ resolve(port);
295
+ };
296
+ portInitState.reject = (err) => {
297
+ cleanupPortInit();
298
+ reject(err);
299
+ };
300
+ portInitState.timer = window.setTimeout(() => {
301
+ portInitState.reject?.(new Error(`MessagePort init timed out after ${timeoutMs}ms`));
302
+ }, timeoutMs);
303
+ });
304
+ try {
305
+ window.LingXiaProxy?.getPort("LingXiaPort");
306
+ }
307
+ catch (e) {
308
+ portInitState.reject?.(e);
309
+ }
310
+ return portInitState.promise;
311
+ }
312
+ function postToNative(message) {
313
+ const kind = message.kind;
314
+ if (kind === "req" || kind === "notify")
315
+ log(`postToNative: ${kind} ${message.method}`);
316
+ if (isDebugEnabled("proto"))
317
+ console.log("→", JSON.stringify(message, null, 2));
318
+ try {
319
+ if (communicationMethod === "webkit") {
320
+ window.webkit?.messageHandlers[NATIVE_HANDLER_NAME]?.postMessage(message);
321
+ return;
322
+ }
323
+ const messageString = stringifyForNative(message);
324
+ if (communicationMethod === MESSAGE_PORT_TYPE && messagePort) {
325
+ messagePort.postMessage(messageString);
326
+ return;
327
+ }
328
+ if (communicationMethod === JS_INTERFACE_TYPE &&
329
+ window.LingXiaProxy?.postMessage) {
330
+ window.LingXiaProxy.postMessage(messageString);
331
+ return;
332
+ }
333
+ warn("Transport not ready");
334
+ }
335
+ catch (e) {
336
+ error("Send error:", e);
337
+ }
338
+ }
339
+ // Handshake state
340
+ let handshakeSessionId = null;
341
+ let handshakeDone = false;
342
+ let helloSent = false;
343
+ let handshakeRetryCount = 0;
344
+ let handshakeTimer = null;
345
+ // Host method schema — populated from handshake `Ready` message.
346
+ // Maps "namespace.method" → "call" | "stream".
347
+ const hostMethodKinds = {};
348
+ // Request tracking
349
+ let requestCounter = 0;
350
+ function createListenerBuckets() {
351
+ return {
352
+ data: new Set(),
353
+ end: new Set(),
354
+ error: new Set(),
355
+ close: new Set(),
356
+ };
357
+ }
358
+ function createStreamHandle(id, cancelFn) {
359
+ const listeners = createListenerBuckets();
360
+ let done = false;
361
+ let resolveResult = () => { };
362
+ let rejectResult = () => { };
363
+ const pendingData = [];
364
+ const pendingReads = [];
365
+ const result = new Promise((resolve, reject) => {
366
+ resolveResult = resolve;
367
+ rejectResult = reject;
368
+ });
369
+ const handle = {
370
+ id,
371
+ result,
372
+ on(event, listener) {
373
+ if (event === "data")
374
+ listeners.data.add(listener);
375
+ if (event === "end")
376
+ listeners.end.add(listener);
377
+ if (event === "error")
378
+ listeners.error.add(listener);
379
+ return this;
380
+ },
381
+ [Symbol.asyncIterator]() {
382
+ return {
383
+ next() {
384
+ if (pendingData.length > 0) {
385
+ return Promise.resolve({
386
+ done: false,
387
+ value: pendingData.shift(),
388
+ });
389
+ }
390
+ if (done) {
391
+ return result.then((finalValue) => ({ done: true, value: finalValue }), (err) => Promise.reject(err));
392
+ }
393
+ return new Promise((resolve, reject) => {
394
+ pendingReads.push({ resolve, reject });
395
+ });
396
+ },
397
+ };
398
+ },
399
+ cancel() {
400
+ if (done)
401
+ return;
402
+ cancelFn();
403
+ },
404
+ _emitData(payload) {
405
+ if (done)
406
+ return;
407
+ if (pendingReads.length > 0) {
408
+ pendingReads.shift().resolve({ done: false, value: payload });
409
+ }
410
+ else {
411
+ pendingData.push(payload);
412
+ }
413
+ for (const listener of listeners.data) {
414
+ try {
415
+ listener(payload);
416
+ }
417
+ catch (e) {
418
+ warn("Stream data listener failed:", e);
419
+ }
420
+ }
421
+ },
422
+ _resolve(resultValue) {
423
+ if (done)
424
+ return;
425
+ done = true;
426
+ resolveResult(resultValue);
427
+ while (pendingReads.length > 0) {
428
+ pendingReads.shift().resolve({ done: true, value: resultValue });
429
+ }
430
+ for (const listener of listeners.end) {
431
+ try {
432
+ listener(resultValue);
433
+ }
434
+ catch (e) {
435
+ warn("Stream end listener failed:", e);
436
+ }
437
+ }
438
+ },
439
+ _reject(err) {
440
+ if (done)
441
+ return;
442
+ done = true;
443
+ rejectResult(err);
444
+ while (pendingReads.length > 0) {
445
+ pendingReads.shift().reject(err);
446
+ }
447
+ for (const listener of listeners.error) {
448
+ try {
449
+ listener(err);
450
+ }
451
+ catch (e) {
452
+ warn("Stream error listener failed:", e);
453
+ }
454
+ }
455
+ },
456
+ };
457
+ return handle;
458
+ }
459
+ function createChannel(id, sendFn, closeFn) {
460
+ const listeners = createListenerBuckets();
461
+ let open = false;
462
+ let outboundSeq = 0;
463
+ let closed = false;
464
+ const pendingData = [];
465
+ const pendingReads = [];
466
+ const channel = {
467
+ id,
468
+ send(payload) {
469
+ if (!open) {
470
+ channel._reject({
471
+ code: BRIDGE_ERROR.STREAM_CLOSED,
472
+ message: `Channel '${id}' is closed`,
473
+ });
474
+ return;
475
+ }
476
+ sendFn(payload, outboundSeq++);
477
+ },
478
+ on(event, listener) {
479
+ if (event === "data")
480
+ listeners.data.add(listener);
481
+ if (event === "close")
482
+ listeners.close.add(listener);
483
+ if (event === "error")
484
+ listeners.error.add(listener);
485
+ return this;
486
+ },
487
+ [Symbol.asyncIterator]() {
488
+ return {
489
+ next() {
490
+ if (pendingData.length > 0) {
491
+ return Promise.resolve({
492
+ done: false,
493
+ value: pendingData.shift(),
494
+ });
495
+ }
496
+ if (closed) {
497
+ return Promise.resolve({ done: true, value: undefined });
498
+ }
499
+ return new Promise((resolve, reject) => {
500
+ pendingReads.push({ resolve, reject });
501
+ });
502
+ },
503
+ };
504
+ },
505
+ close(code, reason) {
506
+ if (!open)
507
+ return;
508
+ open = false;
509
+ closed = true;
510
+ closeFn(code, reason);
511
+ channel._emitClose(code, reason);
512
+ },
513
+ _emitData(payload) {
514
+ if (!open)
515
+ return;
516
+ if (pendingReads.length > 0) {
517
+ pendingReads.shift().resolve({ done: false, value: payload });
518
+ }
519
+ else {
520
+ pendingData.push(payload);
521
+ }
522
+ for (const listener of listeners.data) {
523
+ try {
524
+ listener(payload);
525
+ }
526
+ catch (e) {
527
+ warn("Channel listener failed:", e);
528
+ }
529
+ }
530
+ },
531
+ _emitClose(code, reason) {
532
+ closed = true;
533
+ while (pendingReads.length > 0) {
534
+ pendingReads.shift().resolve({ done: true, value: undefined });
535
+ }
536
+ for (const listener of listeners.close) {
537
+ try {
538
+ listener(code, reason);
539
+ }
540
+ catch (e) {
541
+ warn("Channel close listener failed:", e);
542
+ }
543
+ }
544
+ },
545
+ _reject(err) {
546
+ closed = true;
547
+ while (pendingReads.length > 0) {
548
+ pendingReads.shift().reject(err);
549
+ }
550
+ for (const listener of listeners.error) {
551
+ try {
552
+ listener(err);
553
+ }
554
+ catch (e) {
555
+ warn("Channel error listener failed:", e);
556
+ }
557
+ }
558
+ },
559
+ _markOpen() {
560
+ open = true;
561
+ closed = false;
562
+ },
563
+ _isOpen() {
564
+ return open;
565
+ },
566
+ _nextSeq() {
567
+ return outboundSeq++;
568
+ },
569
+ };
570
+ return channel;
571
+ }
572
+ const pendingReq = new Map();
573
+ const pendingChannels = new Map();
574
+ const activeChannels = new Map();
575
+ const outbox = [];
576
+ // State sync
577
+ let stateRev = -1;
578
+ let pageData = {};
579
+ const dataSubscribers = new Set();
580
+ const subscriberInitStatus = new WeakMap();
581
+ // Logic→View method handlers (for incoming req from native side)
582
+ const viewMethodHandlers = new Map();
583
+ export function registerViewMethodHandler(method, handler) {
584
+ viewMethodHandlers.set(method, handler);
585
+ }
586
+ function inferCap(method) {
587
+ if (method.startsWith("host."))
588
+ return "host";
589
+ const i = method.indexOf(".");
590
+ return i > 0 ? method.slice(0, i) : "page";
591
+ }
592
+ function normalizeParams(params) {
593
+ if (params === null || params === undefined)
594
+ return undefined;
595
+ return params;
596
+ }
597
+ function isTransportReady() {
598
+ if (communicationMethod === MESSAGE_PORT_TYPE)
599
+ return !!messagePort;
600
+ if (useAppleDownstreamTransport())
601
+ return appleDownstreamConnected;
602
+ return (communicationMethod === "webkit" ||
603
+ communicationMethod === JS_INTERFACE_TYPE);
604
+ }
605
+ function canSendAppMessages() {
606
+ return isTransportReady() && handshakeDone;
607
+ }
608
+ function rejectPendingRequest(reqId, err) {
609
+ const info = pendingReq.get(reqId);
610
+ if (info) {
611
+ pendingReq.delete(reqId);
612
+ if (info.timerId !== null)
613
+ clearTimeout(info.timerId);
614
+ const normalized = toBridgeError(err);
615
+ if (info.mode === "stream" && info.stream) {
616
+ info.stream._reject(normalized);
617
+ return;
618
+ }
619
+ info.reject?.(normalized);
620
+ }
621
+ }
622
+ function rejectPendingChannel(id, err) {
623
+ const pending = pendingChannels.get(id);
624
+ if (!pending)
625
+ return;
626
+ pendingChannels.delete(id);
627
+ if (pending.timerId !== null)
628
+ clearTimeout(pending.timerId);
629
+ const normalized = toBridgeError(err);
630
+ pending.channel._reject(normalized);
631
+ pending.reject(normalized);
632
+ }
633
+ function rejectPendingOperation(id, err) {
634
+ if (pendingReq.has(id)) {
635
+ rejectPendingRequest(id, err);
636
+ return;
637
+ }
638
+ if (pendingChannels.has(id)) {
639
+ rejectPendingChannel(id, err);
640
+ }
641
+ }
642
+ function armRequestTimer(reqId) {
643
+ const info = pendingReq.get(reqId);
644
+ if (!info)
645
+ return;
646
+ if (!Number.isFinite(info.timeoutMs) || info.timeoutMs <= 0) {
647
+ if (info.timerId !== null) {
648
+ clearTimeout(info.timerId);
649
+ info.timerId = null;
650
+ }
651
+ return;
652
+ }
653
+ if (info.timerId !== null)
654
+ clearTimeout(info.timerId);
655
+ info.timerId = setTimeout(() => {
656
+ if (!pendingReq.has(reqId))
657
+ return;
658
+ rejectPendingRequest(reqId, {
659
+ code: BRIDGE_ERROR.TIMEOUT,
660
+ message: `'${info.method}' timed out`,
661
+ });
662
+ }, info.timeoutMs);
663
+ }
664
+ function armChannelTimer(id) {
665
+ const pending = pendingChannels.get(id);
666
+ if (!pending)
667
+ return;
668
+ if (pending.timerId !== null)
669
+ clearTimeout(pending.timerId);
670
+ pending.timerId = setTimeout(() => {
671
+ rejectPendingChannel(id, {
672
+ code: BRIDGE_ERROR.TIMEOUT,
673
+ message: `Channel '${pending.topic}' timed out`,
674
+ });
675
+ }, DEFAULT_TIMEOUT_MS);
676
+ }
677
+ function armPendingOperationTimer(id) {
678
+ if (pendingReq.has(id)) {
679
+ armRequestTimer(id);
680
+ return;
681
+ }
682
+ if (pendingChannels.has(id)) {
683
+ armChannelTimer(id);
684
+ }
685
+ }
686
+ function removeOutboxByReqId(reqId) {
687
+ for (let i = outbox.length - 1; i >= 0; i--) {
688
+ if (outbox[i]?.reqId === reqId) {
689
+ outbox.splice(i, 1);
690
+ return true;
691
+ }
692
+ }
693
+ return false;
694
+ }
695
+ function send(msg, reqId) {
696
+ const kind = msg.kind;
697
+ const isHandshake = kind === "hello" || kind === "helloAck" || kind === "ready";
698
+ if (!canSendAppMessages() && !isHandshake) {
699
+ if (outbox.length >= OUTBOX_LIMIT) {
700
+ error("Outbox full");
701
+ if (reqId) {
702
+ rejectPendingOperation(reqId, {
703
+ code: BRIDGE_ERROR.OUTBOX_FULL,
704
+ message: "Bridge outbox is full",
705
+ });
706
+ }
707
+ return;
708
+ }
709
+ outbox.push({ msg, reqId });
710
+ return;
711
+ }
712
+ if (reqId)
713
+ armPendingOperationTimer(reqId);
714
+ postToNative(msg);
715
+ }
716
+ function flushOutbox() {
717
+ if (!canSendAppMessages())
718
+ return;
719
+ while (outbox.length) {
720
+ const item = outbox.shift();
721
+ if (!item)
722
+ continue;
723
+ if (item.reqId)
724
+ armPendingOperationTimer(item.reqId);
725
+ postToNative(item.msg);
726
+ }
727
+ }
728
+ function clearHandshakeTimer() {
729
+ if (handshakeTimer !== null) {
730
+ clearTimeout(handshakeTimer);
731
+ handshakeTimer = null;
732
+ }
733
+ }
734
+ function startHandshake() {
735
+ if (handshakeDone)
736
+ return;
737
+ if (!isTransportReady())
738
+ return;
739
+ clearHandshakeTimer();
740
+ const hello = {
741
+ v: 2,
742
+ kind: "hello",
743
+ nonce: BRIDGE_CONFIG.nonce || "",
744
+ role: "view",
745
+ protocolsSupported: [2],
746
+ };
747
+ helloSent = true;
748
+ postToNative(hello);
749
+ handshakeTimer = setTimeout(() => {
750
+ if (handshakeDone)
751
+ return;
752
+ handshakeRetryCount++;
753
+ if (handshakeRetryCount < HANDSHAKE_MAX_RETRIES) {
754
+ warn(`Handshake timeout (${handshakeRetryCount}/${HANDSHAKE_MAX_RETRIES}), retrying...`);
755
+ helloSent = false;
756
+ startHandshake();
757
+ }
758
+ else {
759
+ error("Handshake failed");
760
+ clearHandshakeTimer();
761
+ helloSent = false;
762
+ handshakeRetryCount = 0;
763
+ while (outbox.length) {
764
+ const item = outbox.shift();
765
+ if (item?.reqId) {
766
+ rejectPendingOperation(item.reqId, {
767
+ code: BRIDGE_ERROR.HANDSHAKE_FAILED,
768
+ message: "Bridge handshake failed",
769
+ });
770
+ }
771
+ }
772
+ for (const id of Array.from(pendingChannels.keys())) {
773
+ rejectPendingChannel(id, {
774
+ code: BRIDGE_ERROR.HANDSHAKE_FAILED,
775
+ message: "Bridge handshake failed",
776
+ });
777
+ }
778
+ }
779
+ }, HANDSHAKE_TIMEOUT_MS);
780
+ }
781
+ function parseIncoming(msg) {
782
+ if (!msg || typeof msg !== "object")
783
+ return null;
784
+ const v = msg.v;
785
+ const kind = msg.kind;
786
+ if (v !== 2 || typeof kind !== "string")
787
+ return null;
788
+ return msg;
789
+ }
790
+ // JSON Patch
791
+ function jsonPointerUnescape(seg) {
792
+ return seg.replace(/~1/g, "/").replace(/~0/g, "~");
793
+ }
794
+ function parseJsonPointer(path) {
795
+ if (path === "")
796
+ return [];
797
+ if (!path.startsWith("/"))
798
+ throw new Error(`Invalid JSON pointer: ${path}`);
799
+ return path.split("/").slice(1).map(jsonPointerUnescape);
800
+ }
801
+ function getContainerAndKey(root, pointer, autoCreate = false) {
802
+ const segments = parseJsonPointer(pointer);
803
+ if (segments.length === 0)
804
+ return { container: { $root: root }, key: "$root" };
805
+ let current = root;
806
+ for (let i = 0; i < segments.length - 1; i++) {
807
+ const seg = segments[i];
808
+ if (Array.isArray(current)) {
809
+ current = current[Number(seg)];
810
+ }
811
+ else if (current && typeof current === "object") {
812
+ const obj = current;
813
+ if (autoCreate && obj[seg] === undefined) {
814
+ const nextSeg = segments[i + 1];
815
+ obj[seg] = nextSeg && /^\d+$/.test(nextSeg) ? [] : {};
816
+ }
817
+ current = obj[seg];
818
+ }
819
+ else {
820
+ throw new Error(`Invalid container at ${seg}`);
821
+ }
822
+ }
823
+ return { container: current, key: segments[segments.length - 1] };
824
+ }
825
+ function applyJsonPatch(target, ops) {
826
+ for (const op of ops) {
827
+ const autoCreate = op.op === "add" || op.op === "replace";
828
+ const { container, key } = getContainerAndKey(target, op.path, autoCreate);
829
+ if (key === "$root") {
830
+ for (const k of Object.keys(target))
831
+ delete target[k];
832
+ if (op.op !== "remove") {
833
+ const v = op.value;
834
+ if (v && typeof v === "object")
835
+ Object.assign(target, v);
836
+ }
837
+ continue;
838
+ }
839
+ if (Array.isArray(container)) {
840
+ const idx = key === "-" ? container.length : Number(key);
841
+ if (!Number.isFinite(idx) || idx < 0)
842
+ throw new Error(`Invalid index`);
843
+ if (op.op === "remove")
844
+ container.splice(idx, 1);
845
+ else if (op.op === "add")
846
+ container.splice(idx, 0, op.value);
847
+ else if (op.op === "replace")
848
+ container[idx] = op.value;
849
+ continue;
850
+ }
851
+ if (!container || typeof container !== "object")
852
+ throw new Error(`Invalid container`);
853
+ const obj = container;
854
+ if (op.op === "remove")
855
+ delete obj[key];
856
+ else
857
+ obj[key] = op.value;
858
+ }
859
+ }
860
+ function notifyStateSubscribers(initial) {
861
+ dataSubscribers.forEach((listener) => {
862
+ try {
863
+ if (!subscriberInitStatus.has(listener)) {
864
+ subscriberInitStatus.set(listener, true);
865
+ listener(deepCopy(pageData), { rev: stateRev, initial: true });
866
+ return;
867
+ }
868
+ listener(deepCopy(pageData), { rev: stateRev, initial });
869
+ }
870
+ catch (e) {
871
+ warn("Subscriber error:", e);
872
+ }
873
+ });
874
+ }
875
+ function subscribeState(callback) {
876
+ if (typeof callback !== "function")
877
+ return () => { };
878
+ dataSubscribers.add(callback);
879
+ if (stateRev >= 0) {
880
+ subscriberInitStatus.set(callback, true);
881
+ try {
882
+ callback(deepCopy(pageData), { rev: stateRev, initial: true });
883
+ }
884
+ catch (e) {
885
+ error("Callback error:", e);
886
+ }
887
+ }
888
+ return () => {
889
+ dataSubscribers.delete(callback);
890
+ subscriberInitStatus.delete(callback);
891
+ };
892
+ }
893
+ function requestStateRecovery(scope) {
894
+ rawBridge.call("state.getSnapshot", { scope }).catch(() => { });
895
+ }
896
+ function applySnapshotFromResult(result) {
897
+ if (!result || typeof result !== "object")
898
+ return false;
899
+ const obj = result;
900
+ if (typeof obj.rev !== "number" || !Number.isFinite(obj.rev))
901
+ return false;
902
+ if (!obj.state || typeof obj.state !== "object")
903
+ return false;
904
+ pageData = obj.state;
905
+ stateRev = obj.rev;
906
+ if (isDebugEnabled("data")) {
907
+ console.group("[LX] snapshot(res)");
908
+ console.log("rev:", stateRev, "state:", deepCopy(pageData));
909
+ console.groupEnd();
910
+ }
911
+ notifyStateSubscribers(true);
912
+ return true;
913
+ }
914
+ function handleIncomingMessage(msg) {
915
+ // Handle native component events (from Android NativeBridge.sendEventToView)
916
+ if (msg && typeof msg === "object") {
917
+ const obj = msg;
918
+ if (obj.type === "event" && obj.name === "nativecomponent" && obj.payload) {
919
+ const payload = obj.payload;
920
+ const componentId = payload.id ||
921
+ payload.componentId;
922
+ if (typeof componentId === "string") {
923
+ const handler = nativeComponentHandlers.get(componentId);
924
+ if (handler) {
925
+ try {
926
+ handler(payload);
927
+ }
928
+ catch (e) {
929
+ error("NC handler error:", e);
930
+ }
931
+ }
932
+ }
933
+ return;
934
+ }
935
+ }
936
+ const message = parseIncoming(msg);
937
+ if (!message) {
938
+ warn("Invalid V2 message:", msg);
939
+ return;
940
+ }
941
+ switch (message.kind) {
942
+ case "helloAck":
943
+ handshakeSessionId = message.sessionId;
944
+ return;
945
+ case "ready":
946
+ if (handshakeSessionId && message.sessionId !== handshakeSessionId) {
947
+ warn("sessionId mismatch");
948
+ return;
949
+ }
950
+ clearHandshakeTimer();
951
+ handshakeDone = true;
952
+ handshakeRetryCount = 0;
953
+ if (message.hostMethods) {
954
+ for (const [k, v] of Object.entries(message.hostMethods)) {
955
+ hostMethodKinds[k] = v;
956
+ }
957
+ }
958
+ if (isDebugEnabled("proto"))
959
+ log("Handshake complete, hostMethods:", Object.keys(hostMethodKinds).length);
960
+ flushOutbox();
961
+ return;
962
+ case "res": {
963
+ const info = pendingReq.get(message.id);
964
+ if (!info)
965
+ return;
966
+ pendingReq.delete(message.id);
967
+ if (info.timerId !== null)
968
+ clearTimeout(info.timerId);
969
+ if (message.ok) {
970
+ if (info.method === "state.getSnapshot") {
971
+ if (!applySnapshotFromResult(message.result)) {
972
+ warn("Invalid state.getSnapshot result");
973
+ }
974
+ }
975
+ if (info.mode === "stream" && info.stream) {
976
+ info.stream._resolve(message.result);
977
+ return;
978
+ }
979
+ info.resolve?.(message.result);
980
+ }
981
+ else {
982
+ const err = toBridgeError(message.error);
983
+ if (info.mode === "stream" && info.stream) {
984
+ info.stream._reject(err);
985
+ return;
986
+ }
987
+ info.reject?.(err);
988
+ }
989
+ return;
990
+ }
991
+ case "event": {
992
+ if (pendingReq.has(message.id)) {
993
+ const req = pendingReq.get(message.id);
994
+ if (req?.mode === "stream" && req.stream) {
995
+ armRequestTimer(message.id);
996
+ req.stream._emitData(message.payload);
997
+ }
998
+ else {
999
+ warn(`Received event for non-stream request '${message.id}'`);
1000
+ }
1001
+ return;
1002
+ }
1003
+ return;
1004
+ }
1005
+ case "state.snapshot":
1006
+ pageData = message.state || {};
1007
+ stateRev = message.rev;
1008
+ if (isDebugEnabled("data")) {
1009
+ console.group("[LX] snapshot");
1010
+ console.log("rev:", stateRev, "state:", deepCopy(pageData));
1011
+ console.groupEnd();
1012
+ }
1013
+ notifyStateSubscribers(true);
1014
+ return;
1015
+ case "state.patch":
1016
+ if (message.baseRev !== stateRev) {
1017
+ warn("baseRev mismatch", { have: stateRev, want: message.baseRev });
1018
+ requestStateRecovery(message.scope);
1019
+ return;
1020
+ }
1021
+ try {
1022
+ applyJsonPatch(pageData, message.ops || []);
1023
+ stateRev = message.rev;
1024
+ }
1025
+ catch (e) {
1026
+ error("Patch failed:", e);
1027
+ requestStateRecovery(message.scope);
1028
+ return;
1029
+ }
1030
+ if (isDebugEnabled("data")) {
1031
+ console.group("[LX] patch");
1032
+ console.log("rev:", stateRev, "ops:", message.ops);
1033
+ console.groupEnd();
1034
+ }
1035
+ notifyStateSubscribers(false);
1036
+ if (message.ack)
1037
+ send({
1038
+ v: 2,
1039
+ kind: "state.ack",
1040
+ scope: message.scope,
1041
+ rev: message.rev,
1042
+ });
1043
+ return;
1044
+ case "ch.ack": {
1045
+ const pendingChannel = pendingChannels.get(message.id);
1046
+ if (!pendingChannel)
1047
+ return;
1048
+ pendingChannels.delete(message.id);
1049
+ if (pendingChannel.timerId !== null)
1050
+ clearTimeout(pendingChannel.timerId);
1051
+ if (message.ok) {
1052
+ pendingChannel.channel._markOpen();
1053
+ activeChannels.set(message.id, pendingChannel.channel);
1054
+ pendingChannel.resolve(pendingChannel.channel);
1055
+ }
1056
+ else {
1057
+ const err = toBridgeError(message.error);
1058
+ pendingChannel.channel._reject(err);
1059
+ pendingChannel.reject(err);
1060
+ }
1061
+ return;
1062
+ }
1063
+ case "ch.data": {
1064
+ const channel = activeChannels.get(message.id);
1065
+ if (!channel)
1066
+ return;
1067
+ channel._emitData(message.payload);
1068
+ return;
1069
+ }
1070
+ case "ch.close": {
1071
+ const pendingChannel = pendingChannels.get(message.id);
1072
+ if (pendingChannel) {
1073
+ pendingChannels.delete(message.id);
1074
+ pendingChannel.channel._emitClose(message.code, message.reason);
1075
+ return;
1076
+ }
1077
+ const channel = activeChannels.get(message.id);
1078
+ if (!channel)
1079
+ return;
1080
+ activeChannels.delete(message.id);
1081
+ channel._emitClose(message.code, message.reason);
1082
+ return;
1083
+ }
1084
+ case "req": {
1085
+ const reqMsg = message;
1086
+ const requiredCap = inferCap(reqMsg.method);
1087
+ if (!reqMsg.cap || reqMsg.cap !== requiredCap) {
1088
+ send({
1089
+ v: 2,
1090
+ kind: "res",
1091
+ id: reqMsg.id,
1092
+ ok: false,
1093
+ error: {
1094
+ code: BRIDGE_ERROR.CAPABILITY_DENIED,
1095
+ message: `Invalid cap for ${reqMsg.method}`,
1096
+ },
1097
+ });
1098
+ return;
1099
+ }
1100
+ const handler = viewMethodHandlers.get(reqMsg.method);
1101
+ if (!handler) {
1102
+ send({
1103
+ v: 2,
1104
+ kind: "res",
1105
+ id: reqMsg.id,
1106
+ ok: false,
1107
+ error: {
1108
+ code: BRIDGE_ERROR.METHOD_NOT_FOUND,
1109
+ message: `View handler not found: ${reqMsg.method}`,
1110
+ },
1111
+ });
1112
+ return;
1113
+ }
1114
+ Promise.resolve()
1115
+ .then(() => handler(reqMsg.params))
1116
+ .then((result) => {
1117
+ send({ v: 2, kind: "res", id: reqMsg.id, ok: true, result });
1118
+ })
1119
+ .catch((err) => {
1120
+ send({
1121
+ v: 2,
1122
+ kind: "res",
1123
+ id: reqMsg.id,
1124
+ ok: false,
1125
+ error: toBridgeError(err),
1126
+ });
1127
+ });
1128
+ return;
1129
+ }
1130
+ }
1131
+ }
1132
+ // Native components
1133
+ const nativeComponentHandlers = new Map();
1134
+ const nativeComponentQueue = [];
1135
+ let nativeComponentReady = false;
1136
+ function hasNativeComponentHandler() {
1137
+ if (typeof window === "undefined")
1138
+ return false;
1139
+ return !!(window.webkit?.messageHandlers?.NativeComponent ||
1140
+ window.NativeComponentBridge?.postMessage);
1141
+ }
1142
+ function postNativeComponentMessage(message) {
1143
+ try {
1144
+ if (window.webkit?.messageHandlers?.NativeComponent) {
1145
+ window.webkit.messageHandlers.NativeComponent.postMessage(message);
1146
+ return;
1147
+ }
1148
+ if (window.NativeComponentBridge?.postMessage) {
1149
+ window.NativeComponentBridge.postMessage(stringifyForNative(message));
1150
+ return;
1151
+ }
1152
+ }
1153
+ catch (e) {
1154
+ error("NativeComponent send error:", e);
1155
+ }
1156
+ }
1157
+ function flushNativeComponentQueue() {
1158
+ if (!hasNativeComponentHandler() || nativeComponentQueue.length === 0)
1159
+ return;
1160
+ nativeComponentReady = true;
1161
+ while (nativeComponentQueue.length) {
1162
+ const msg = nativeComponentQueue.shift();
1163
+ try {
1164
+ postNativeComponentMessage(msg);
1165
+ }
1166
+ catch {
1167
+ break;
1168
+ }
1169
+ }
1170
+ }
1171
+ function sendNativeComponentMessage(message) {
1172
+ try {
1173
+ if (!hasNativeComponentHandler()) {
1174
+ nativeComponentQueue.push(message);
1175
+ return;
1176
+ }
1177
+ if (!nativeComponentReady)
1178
+ flushNativeComponentQueue();
1179
+ postNativeComponentMessage(message);
1180
+ }
1181
+ catch (e) {
1182
+ error("NC send failed:", e);
1183
+ }
1184
+ }
1185
+ function nextMessageId(prefix) {
1186
+ return `${prefix}_${Date.now()}_${requestCounter++}`;
1187
+ }
1188
+ function attachAbortSignal(id, signal, onAbort) {
1189
+ if (!signal)
1190
+ return;
1191
+ if (signal.aborted) {
1192
+ onAbort();
1193
+ return;
1194
+ }
1195
+ let abortListener;
1196
+ abortListener = {
1197
+ handleEvent: () => {
1198
+ signal.removeEventListener("abort", abortListener);
1199
+ onAbort();
1200
+ },
1201
+ };
1202
+ signal.addEventListener("abort", abortListener);
1203
+ }
1204
+ // Low-level protocol interface. Public consumers should use LingXiaBridge.invoke/stream/channel.
1205
+ const rawBridge = {
1206
+ call(method, params, options) {
1207
+ return new Promise((resolve, reject) => {
1208
+ if (!method || typeof method !== "string") {
1209
+ reject({
1210
+ code: BRIDGE_ERROR.MALFORMED_MESSAGE,
1211
+ message: "Method name must be a non-empty string",
1212
+ });
1213
+ return;
1214
+ }
1215
+ if (!helloSent)
1216
+ startHandshake();
1217
+ const id = nextMessageId("c");
1218
+ const cap = options?.cap || inferCap(method);
1219
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1220
+ pendingReq.set(id, {
1221
+ method,
1222
+ mode: "call",
1223
+ resolve,
1224
+ reject: (e) => reject(e),
1225
+ timeoutMs,
1226
+ timerId: null,
1227
+ });
1228
+ const req = {
1229
+ v: 2,
1230
+ kind: "req",
1231
+ id,
1232
+ method,
1233
+ params: normalizeParams(params),
1234
+ cap,
1235
+ };
1236
+ send(req, id);
1237
+ attachAbortSignal(id, options?.signal, () => {
1238
+ const removed = removeOutboxByReqId(id);
1239
+ rejectPendingRequest(id, {
1240
+ code: BRIDGE_ERROR.CANCELED,
1241
+ message: "Bridge request aborted",
1242
+ });
1243
+ if (!removed && handshakeDone)
1244
+ send({ v: 2, kind: "cancel", id });
1245
+ });
1246
+ });
1247
+ },
1248
+ stream(method, params, options) {
1249
+ if (!method || typeof method !== "string") {
1250
+ const invalid = createStreamHandle("invalid", () => { });
1251
+ invalid._reject({
1252
+ code: BRIDGE_ERROR.MALFORMED_MESSAGE,
1253
+ message: "Method name must be a non-empty string",
1254
+ });
1255
+ return invalid;
1256
+ }
1257
+ if (!helloSent)
1258
+ startHandshake();
1259
+ const id = nextMessageId("c");
1260
+ const handle = createStreamHandle(id, () => {
1261
+ const removed = removeOutboxByReqId(id);
1262
+ if (removed) {
1263
+ rejectPendingRequest(id, {
1264
+ code: BRIDGE_ERROR.CANCELED,
1265
+ message: "Bridge request aborted",
1266
+ });
1267
+ return;
1268
+ }
1269
+ if (handshakeDone)
1270
+ send({ v: 2, kind: "cancel", id });
1271
+ });
1272
+ const cap = options?.cap || inferCap(method);
1273
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1274
+ pendingReq.set(id, {
1275
+ method,
1276
+ mode: "stream",
1277
+ stream: handle,
1278
+ timeoutMs,
1279
+ timerId: null,
1280
+ });
1281
+ send({
1282
+ v: 2,
1283
+ kind: "req",
1284
+ id,
1285
+ method,
1286
+ params: normalizeParams(params),
1287
+ cap,
1288
+ }, id);
1289
+ attachAbortSignal(id, options?.signal, () => handle.cancel());
1290
+ return handle;
1291
+ },
1292
+ notify(method, params, options) {
1293
+ if (!method || typeof method !== "string")
1294
+ return;
1295
+ if (!helloSent)
1296
+ startHandshake();
1297
+ send({
1298
+ v: 2,
1299
+ kind: "notify",
1300
+ method,
1301
+ params: normalizeParams(params),
1302
+ cap: options?.cap || inferCap(method),
1303
+ });
1304
+ },
1305
+ state: {
1306
+ subscribe: subscribeState,
1307
+ },
1308
+ channel: {
1309
+ open(topic, params, options) {
1310
+ if (!topic || typeof topic !== "string") {
1311
+ return Promise.reject({
1312
+ code: BRIDGE_ERROR.MALFORMED_MESSAGE,
1313
+ message: "Topic name must be a non-empty string",
1314
+ });
1315
+ }
1316
+ if (!helloSent)
1317
+ startHandshake();
1318
+ return new Promise((resolve, reject) => {
1319
+ const id = nextMessageId("ch");
1320
+ const channel = createChannel(id, (payload, seq) => {
1321
+ send({ v: 2, kind: "ch.data", id, seq, payload });
1322
+ }, (code, reason) => {
1323
+ const pending = pendingChannels.get(id);
1324
+ if (pending && pending.timerId !== null)
1325
+ clearTimeout(pending.timerId);
1326
+ pendingChannels.delete(id);
1327
+ activeChannels.delete(id);
1328
+ send({ v: 2, kind: "ch.close", id, code, reason });
1329
+ });
1330
+ pendingChannels.set(id, {
1331
+ topic,
1332
+ channel,
1333
+ resolve: resolve,
1334
+ reject,
1335
+ timerId: null,
1336
+ });
1337
+ send({
1338
+ v: 2,
1339
+ kind: "ch.open",
1340
+ id,
1341
+ topic,
1342
+ params: normalizeParams(params),
1343
+ cap: options?.cap || inferCap(topic),
1344
+ }, id);
1345
+ });
1346
+ },
1347
+ },
1348
+ };
1349
+ // Public interface
1350
+ export const LingXiaBridge = {
1351
+ invoke(route, input, options) {
1352
+ return rawBridge
1353
+ .call(nativeRoute(route), input, nativeOptions(options))
1354
+ .then((value) => value)
1355
+ .catch((error) => Promise.reject(toNativeError(error)));
1356
+ },
1357
+ notify(route, input, options) {
1358
+ rawBridge.notify(nativeRoute(route), input, nativeOptions(options));
1359
+ },
1360
+ stream(route, input, options) {
1361
+ return wrapNativeStream(rawBridge.stream(nativeRoute(route), input, nativeOptions(options)));
1362
+ },
1363
+ channel(route, input, options) {
1364
+ return rawBridge.channel
1365
+ .open(nativeRoute(route), input, nativeOptions(options))
1366
+ .then((handle) => wrapNativeChannel(handle))
1367
+ .catch((error) => Promise.reject(toNativeError(error)));
1368
+ },
1369
+ raw: rawBridge,
1370
+ state: {
1371
+ subscribe: subscribeState,
1372
+ },
1373
+ _connectWebMessagePort(port) {
1374
+ if (communicationMethod !== MESSAGE_PORT_TYPE)
1375
+ return;
1376
+ if (messagePort && messagePort !== port) {
1377
+ try {
1378
+ messagePort.onmessage = null;
1379
+ messagePort.close();
1380
+ }
1381
+ catch { }
1382
+ }
1383
+ messagePort = port;
1384
+ port.onmessage = (event) => {
1385
+ let data = event.data;
1386
+ if (typeof data === "string") {
1387
+ try {
1388
+ data = JSON.parse(data);
1389
+ }
1390
+ catch {
1391
+ return;
1392
+ }
1393
+ }
1394
+ handleIncomingMessage(data);
1395
+ };
1396
+ // Some WebView MessagePort implementations (notably Android WebMessagePort)
1397
+ // require an explicit start() to begin dispatching onmessage events.
1398
+ try {
1399
+ port.start();
1400
+ }
1401
+ catch { }
1402
+ log("Port connected");
1403
+ startHandshake();
1404
+ },
1405
+ _receiveEvaluateMessage(messageString) {
1406
+ try {
1407
+ if (messageString)
1408
+ handleIncomingMessage(JSON.parse(messageString));
1409
+ }
1410
+ catch (e) {
1411
+ error("Parse error:", e);
1412
+ }
1413
+ },
1414
+ debug: new Proxy(debugFlags, {
1415
+ get(target, prop) {
1416
+ return target[prop];
1417
+ },
1418
+ set(target, prop, value) {
1419
+ if (prop in target) {
1420
+ target[prop] = !!value;
1421
+ console.log(`[LX] ${prop}: ${value}`);
1422
+ return true;
1423
+ }
1424
+ return false;
1425
+ },
1426
+ }),
1427
+ platform: {
1428
+ isHarmony,
1429
+ isIOS,
1430
+ isAndroid,
1431
+ isMacOS,
1432
+ isDesktop,
1433
+ getOS: getPlatformOS,
1434
+ },
1435
+ dom: {
1436
+ measureById(id) {
1437
+ try {
1438
+ if (!id)
1439
+ return null;
1440
+ const el = document.getElementById(id);
1441
+ if (!el)
1442
+ return null;
1443
+ const r = el.getBoundingClientRect();
1444
+ let radius = 0;
1445
+ try {
1446
+ radius = parseFloat(getComputedStyle(el).borderRadius) || 0;
1447
+ }
1448
+ catch { }
1449
+ return [
1450
+ r.left + window.scrollX,
1451
+ r.top + window.scrollY,
1452
+ r.width,
1453
+ r.height,
1454
+ radius,
1455
+ ];
1456
+ }
1457
+ catch {
1458
+ return null;
1459
+ }
1460
+ },
1461
+ },
1462
+ nativeComponents: {
1463
+ send: sendNativeComponentMessage,
1464
+ hasHandler: hasNativeComponentHandler,
1465
+ flush: flushNativeComponentQueue,
1466
+ register(id, handler) {
1467
+ if (!id || typeof handler !== "function")
1468
+ return () => { };
1469
+ nativeComponentHandlers.set(id, handler);
1470
+ return () => nativeComponentHandlers.delete(id);
1471
+ },
1472
+ unregister(id) {
1473
+ nativeComponentHandlers.delete(id);
1474
+ },
1475
+ },
1476
+ isReady() {
1477
+ return handshakeDone;
1478
+ },
1479
+ };
1480
+ function nativeRoute(route) {
1481
+ return route.startsWith("host.") ? route : `host.${route}`;
1482
+ }
1483
+ function nativeOptions(options) {
1484
+ return { ...(options || {}), cap: options?.cap || "host" };
1485
+ }
1486
+ function wrapNativeStream(handle) {
1487
+ const eventListeners = new Set();
1488
+ const errorListeners = new Set();
1489
+ handle.on("data", (event) => {
1490
+ for (const listener of eventListeners)
1491
+ listener(event);
1492
+ });
1493
+ handle.on("error", (error) => {
1494
+ const nativeError = toNativeError(error);
1495
+ for (const listener of errorListeners)
1496
+ listener(nativeError);
1497
+ });
1498
+ return {
1499
+ onEvent(listener) {
1500
+ eventListeners.add(listener);
1501
+ return () => {
1502
+ eventListeners.delete(listener);
1503
+ };
1504
+ },
1505
+ onError(listener) {
1506
+ errorListeners.add(listener);
1507
+ return () => {
1508
+ errorListeners.delete(listener);
1509
+ };
1510
+ },
1511
+ result: handle.result.catch((error) => Promise.reject(toNativeError(error))),
1512
+ cancel() {
1513
+ handle.cancel();
1514
+ },
1515
+ };
1516
+ }
1517
+ function wrapNativeChannel(channel) {
1518
+ const messageListeners = new Set();
1519
+ const closeListeners = new Set();
1520
+ channel.on("data", (message) => {
1521
+ for (const listener of messageListeners)
1522
+ listener(message);
1523
+ });
1524
+ channel.on("close", (code, reason) => {
1525
+ const event = { code, reason };
1526
+ for (const listener of closeListeners)
1527
+ listener(event);
1528
+ });
1529
+ return {
1530
+ send(message) {
1531
+ channel.send(message);
1532
+ },
1533
+ onMessage(listener) {
1534
+ messageListeners.add(listener);
1535
+ return () => {
1536
+ messageListeners.delete(listener);
1537
+ };
1538
+ },
1539
+ onClose(listener) {
1540
+ closeListeners.add(listener);
1541
+ return () => {
1542
+ closeListeners.delete(listener);
1543
+ };
1544
+ },
1545
+ close(code, reason) {
1546
+ channel.close(code, reason);
1547
+ },
1548
+ };
1549
+ }
1550
+ export function invoke(route, input, options) {
1551
+ return LingXiaBridge.invoke(route, input, options);
1552
+ }
1553
+ export function notify(route, input, options) {
1554
+ rawBridge.notify(nativeRoute(route), input, nativeOptions(options));
1555
+ }
1556
+ export function stream(route, input, options) {
1557
+ return wrapNativeStream(rawBridge.stream(nativeRoute(route), input, nativeOptions(options)));
1558
+ }
1559
+ export function channel(route, input, options) {
1560
+ return LingXiaBridge.channel(route, input, options);
1561
+ }
1562
+ export function initBridge() {
1563
+ if (window.__LX_BRIDGE_INIT_STATE) {
1564
+ log(`Bridge already ${window.__LX_BRIDGE_INIT_STATE}, skipping duplicate init`);
1565
+ return;
1566
+ }
1567
+ window.__LX_BRIDGE_INIT_STATE = "initializing";
1568
+ try {
1569
+ log(`Method: ${communicationMethod}`);
1570
+ activateReceiver(LingXiaBridge._receiveEvaluateMessage);
1571
+ if (useAppleDownstreamTransport()) {
1572
+ ensureAppleDownstream();
1573
+ }
1574
+ else if (communicationMethod === MESSAGE_PORT_TYPE) {
1575
+ installMessagePortInitListener();
1576
+ getMessagePort().catch((e) => warn("Port init failed:", e));
1577
+ }
1578
+ else if (communicationMethod === "webkit" ||
1579
+ communicationMethod === JS_INTERFACE_TYPE) {
1580
+ startHandshake();
1581
+ }
1582
+ else {
1583
+ warn("Unknown method");
1584
+ }
1585
+ window.LingXiaBridge = LingXiaBridge;
1586
+ installNativeComponentCoverageMonitor({
1587
+ os: getPlatformOS(),
1588
+ send: sendNativeComponentMessage,
1589
+ });
1590
+ window.__LX_BRIDGE_INIT_STATE = "initialized";
1591
+ log("Init complete");
1592
+ }
1593
+ catch (e) {
1594
+ delete window.__LX_BRIDGE_INIT_STATE;
1595
+ throw e;
1596
+ }
1597
+ }