@lingxia/bridge 0.14.0 → 0.16.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.
- package/dist/bridge-runtime.es2020.js +1 -1
- package/dist/bridge-runtime.es5.js +1 -1
- package/dist/es2020/bridge.js +317 -66
- package/dist/es2020/protocol-v3.js +191 -0
- package/dist/es2020/runtime-env.js +32 -3
- package/dist/es2020/types.js +1 -0
- package/dist/es5/bridge.js +430 -143
- package/dist/es5/protocol-v3.js +215 -0
- package/dist/es5/runtime-env.js +32 -3
- package/dist/es5/types.js +1 -0
- package/dist/protocol-v3.d.ts +43 -0
- package/dist/runtime-env.d.ts +10 -2
- package/dist/types.d.ts +34 -0
- package/package.json +3 -1
package/dist/es2020/bridge.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { BRIDGE_ERROR } from "./types";
|
|
2
2
|
import { toBridgeError, toNativeError } from "./invocation";
|
|
3
3
|
import { installNativeComponentCoverageMonitor } from "./nativecomponents/coverage-monitor";
|
|
4
|
-
import {
|
|
4
|
+
import { consumeV3Bootstrap, } from "./protocol-v3";
|
|
5
|
+
import { BRIDGE_CONFIG, getCommunicationMethod, getDisplayLanguage, getPlatformOS, subscribeDisplayLanguage, isAndroid, isHarmony, isIOS, isMacOS, isWindows, isDesktop, isMobile, isApple, isDevSession, isRunner, } from "./runtime-env";
|
|
5
6
|
const NATIVE_HANDLER_NAME = "LingXia";
|
|
6
7
|
const GLOBAL_RECEIVER_NAME = "__LingXiaRecvMessage";
|
|
7
8
|
const DEFAULT_TIMEOUT_MS = 5000;
|
|
@@ -15,8 +16,28 @@ const OUTBOX_LIMIT = 256;
|
|
|
15
16
|
const APPLE_DOWNSTREAM_URL = BRIDGE_CONFIG.appleDownstreamURL;
|
|
16
17
|
const APPLE_RECONNECT_BASE_MS = 200;
|
|
17
18
|
const APPLE_RECONNECT_MAX_MS = 2000;
|
|
19
|
+
const BOUND_CONSOLE_MESSAGE_CHARS = 12 * 1024;
|
|
20
|
+
// Native drops a larger frame before it reaches the bridge, so a request that
|
|
21
|
+
// exceeds this would otherwise only surface as a timeout. Mirrors
|
|
22
|
+
// MAX_WEB_MESSAGE_BYTES in the webview crate.
|
|
23
|
+
const MAX_NATIVE_MESSAGE_BYTES = 64 * 1024;
|
|
24
|
+
// Keep framework diagnostics and console forwarding on the original methods.
|
|
25
|
+
// RequiredV3 installs a page-facing wrapper later; using it here would recurse
|
|
26
|
+
// and could accidentally serialize the bound envelope into another log.
|
|
27
|
+
const nativeConsole = {
|
|
28
|
+
log: console.log.bind(console),
|
|
29
|
+
warn: console.warn.bind(console),
|
|
30
|
+
error: console.error.bind(console),
|
|
31
|
+
info: (console.info || console.log).bind(console),
|
|
32
|
+
debug: (console.debug || console.log).bind(console),
|
|
33
|
+
group: (console.group || console.log).bind(console),
|
|
34
|
+
groupEnd: (console.groupEnd || (() => { })).bind(console),
|
|
35
|
+
};
|
|
18
36
|
const debugFlags = { data: false, proto: false, all: false };
|
|
19
37
|
const earlyNativeMessages = [];
|
|
38
|
+
// Fixed once, during init. The V3 secret remains captured only by the codec.
|
|
39
|
+
let protocolMode = { kind: "v2" };
|
|
40
|
+
let protocolInitialized = false;
|
|
20
41
|
// Plain-object equivalent of `new Proxy(debugFlags, ...)`. Avoids referencing
|
|
21
42
|
// the `Proxy` global so the module loads on older WebViews (Android 5.x stock
|
|
22
43
|
// WebView is Chromium 37–39, which predates Chromium 49's Proxy support).
|
|
@@ -31,25 +52,50 @@ function createDebugObject(flags) {
|
|
|
31
52
|
},
|
|
32
53
|
set(value) {
|
|
33
54
|
flags[key] = !!value;
|
|
34
|
-
|
|
55
|
+
nativeConsole.log(`[LX] ${key}: ${value}`);
|
|
35
56
|
},
|
|
36
57
|
});
|
|
37
58
|
});
|
|
38
59
|
return target;
|
|
39
60
|
}
|
|
61
|
+
function drainHostEarlyNativeMessages() {
|
|
62
|
+
if (typeof window === "undefined")
|
|
63
|
+
return;
|
|
64
|
+
const hostQueue = window.__LingXiaEarlyNativeMessages;
|
|
65
|
+
if (!Array.isArray(hostQueue) || hostQueue.length === 0)
|
|
66
|
+
return;
|
|
67
|
+
window.__LingXiaEarlyNativeMessages = [];
|
|
68
|
+
for (const message of hostQueue) {
|
|
69
|
+
if (typeof message === "string") {
|
|
70
|
+
earlyNativeMessages.push(message);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
40
74
|
function installEarlyReceiver() {
|
|
41
75
|
if (typeof window === "undefined")
|
|
42
76
|
return;
|
|
43
|
-
|
|
77
|
+
drainHostEarlyNativeMessages();
|
|
78
|
+
const existing = window[GLOBAL_RECEIVER_NAME];
|
|
79
|
+
if (typeof existing === "function") {
|
|
80
|
+
if (earlyNativeMessages.length === 0)
|
|
81
|
+
return;
|
|
82
|
+
const queued = earlyNativeMessages.splice(0, earlyNativeMessages.length);
|
|
83
|
+
for (const message of queued) {
|
|
84
|
+
try {
|
|
85
|
+
existing(message);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
warn("Failed to replay queued native message:", err);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
44
91
|
return;
|
|
92
|
+
}
|
|
45
93
|
window[GLOBAL_RECEIVER_NAME] = (message) => {
|
|
46
94
|
earlyNativeMessages.push(message);
|
|
47
95
|
};
|
|
48
96
|
}
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
return;
|
|
52
|
-
window[GLOBAL_RECEIVER_NAME] = receiver;
|
|
97
|
+
function flushEarlyNativeMessages(receiver) {
|
|
98
|
+
drainHostEarlyNativeMessages();
|
|
53
99
|
if (earlyNativeMessages.length === 0)
|
|
54
100
|
return;
|
|
55
101
|
const queued = earlyNativeMessages.splice(0, earlyNativeMessages.length);
|
|
@@ -62,6 +108,17 @@ function activateReceiver(receiver) {
|
|
|
62
108
|
}
|
|
63
109
|
}
|
|
64
110
|
}
|
|
111
|
+
function activateReceiver(receiver) {
|
|
112
|
+
if (typeof window === "undefined")
|
|
113
|
+
return;
|
|
114
|
+
// Drain the inject-script queue on every inbound call so late
|
|
115
|
+
// `__LingXiaEarlyNativeMessages` items flush without rebinding.
|
|
116
|
+
window[GLOBAL_RECEIVER_NAME] = (message) => {
|
|
117
|
+
flushEarlyNativeMessages(receiver);
|
|
118
|
+
receiver(message);
|
|
119
|
+
};
|
|
120
|
+
flushEarlyNativeMessages(receiver);
|
|
121
|
+
}
|
|
65
122
|
function isDebugEnabled(flag) {
|
|
66
123
|
return debugFlags.all || debugFlags[flag];
|
|
67
124
|
}
|
|
@@ -77,13 +134,13 @@ function log(...args) {
|
|
|
77
134
|
!debugFlags.data) {
|
|
78
135
|
return;
|
|
79
136
|
}
|
|
80
|
-
|
|
137
|
+
nativeConsole.log(LOG_PREFIX, ...args);
|
|
81
138
|
}
|
|
82
139
|
function warn(...args) {
|
|
83
|
-
|
|
140
|
+
nativeConsole.warn(LOG_PREFIX, ...args);
|
|
84
141
|
}
|
|
85
142
|
function error(...args) {
|
|
86
|
-
|
|
143
|
+
nativeConsole.error(LOG_PREFIX, ...args);
|
|
87
144
|
}
|
|
88
145
|
function safeStringify(obj, space) {
|
|
89
146
|
const seen = new WeakSet();
|
|
@@ -132,7 +189,15 @@ let appleLastFrameSeq = 0;
|
|
|
132
189
|
// than a couple of heartbeats means the connection is silently dead and we
|
|
133
190
|
// reconnect proactively instead of waiting for a read error that may never come.
|
|
134
191
|
const APPLE_DOWNSTREAM_STALE_MS = 35000;
|
|
192
|
+
// A downstream that has never delivered a frame gets this long to connect.
|
|
193
|
+
const APPLE_DOWNSTREAM_CONNECT_TIMEOUT_MS = 10000;
|
|
135
194
|
let appleLastFrameAt = 0;
|
|
195
|
+
let appleDownstreamAttemptAt = 0;
|
|
196
|
+
// Names this page load to the host transport, so a reloaded document never
|
|
197
|
+
// resumes the sequence of the one it replaced.
|
|
198
|
+
const APPLE_DOWNSTREAM_DOCUMENT = `${Date.now().toString(36)}${Math.random()
|
|
199
|
+
.toString(36)
|
|
200
|
+
.slice(2, 10)}`;
|
|
136
201
|
let appleWatchdogTimer = null;
|
|
137
202
|
const portInitState = {
|
|
138
203
|
listenerInstalled: false,
|
|
@@ -217,7 +282,9 @@ function rejectAllPendingForTransport(reason) {
|
|
|
217
282
|
}
|
|
218
283
|
function resetHandshakeState(reason, rejectPending) {
|
|
219
284
|
clearHandshakeTimer();
|
|
220
|
-
|
|
285
|
+
if (protocolMode.kind !== "required-v3")
|
|
286
|
+
handshakeSessionId = null;
|
|
287
|
+
helloAcknowledged = false;
|
|
221
288
|
handshakeDone = false;
|
|
222
289
|
helloSent = false;
|
|
223
290
|
handshakeRetryCount = 0;
|
|
@@ -284,7 +351,8 @@ async function runAppleDownstream() {
|
|
|
284
351
|
appleDownstreamAbortController = controller;
|
|
285
352
|
// Resume from the last frame we saw so the host replays the gap on reconnect.
|
|
286
353
|
const separator = APPLE_DOWNSTREAM_URL.includes("?") ? "&" : "?";
|
|
287
|
-
const url = `${APPLE_DOWNSTREAM_URL}${separator}from=${appleLastFrameSeq}`;
|
|
354
|
+
const url = `${APPLE_DOWNSTREAM_URL}${separator}from=${appleLastFrameSeq}&doc=${APPLE_DOWNSTREAM_DOCUMENT}`;
|
|
355
|
+
appleDownstreamAttemptAt = Date.now();
|
|
288
356
|
const response = await fetch(url, {
|
|
289
357
|
method: "GET",
|
|
290
358
|
cache: "no-store",
|
|
@@ -430,8 +498,18 @@ function startAppleWatchdog() {
|
|
|
430
498
|
appleWatchdogTimer = setInterval(() => {
|
|
431
499
|
// No `connected` guard: a reconnect that hung before delivering a frame
|
|
432
500
|
// also goes stale, and forcing a reconnect aborts and retries it.
|
|
433
|
-
if (appleLastFrameAt === 0)
|
|
501
|
+
if (appleLastFrameAt === 0) {
|
|
502
|
+
// The first downstream has produced nothing yet. A fetch whose response
|
|
503
|
+
// never arrives raises no error, and a page that stays in-window (under
|
|
504
|
+
// a splash, say) never sees the visibility handler either.
|
|
505
|
+
if (appleDownstreamTask &&
|
|
506
|
+
appleDownstreamAttemptAt !== 0 &&
|
|
507
|
+
Date.now() - appleDownstreamAttemptAt > APPLE_DOWNSTREAM_CONNECT_TIMEOUT_MS) {
|
|
508
|
+
warn("Apple downstream did not connect, forcing reconnect");
|
|
509
|
+
forceDownstreamReconnect();
|
|
510
|
+
}
|
|
434
511
|
return;
|
|
512
|
+
}
|
|
435
513
|
if (Date.now() - appleLastFrameAt > APPLE_DOWNSTREAM_STALE_MS) {
|
|
436
514
|
warn("Apple downstream stale (no heartbeat), forcing reconnect");
|
|
437
515
|
forceDownstreamReconnect();
|
|
@@ -477,43 +555,114 @@ function getMessagePort() {
|
|
|
477
555
|
}
|
|
478
556
|
return portInitState.promise;
|
|
479
557
|
}
|
|
558
|
+
function encodeForNative(message) {
|
|
559
|
+
if (protocolMode.kind === "blocked")
|
|
560
|
+
return null;
|
|
561
|
+
if (protocolMode.kind === "v2")
|
|
562
|
+
return message;
|
|
563
|
+
if (!message || typeof message !== "object")
|
|
564
|
+
return null;
|
|
565
|
+
const { v: _v, kind, ...payload } = message;
|
|
566
|
+
if (typeof kind !== "string")
|
|
567
|
+
return null;
|
|
568
|
+
const encoded = protocolMode.codec.encode(kind, payload);
|
|
569
|
+
return encoded.ok ? encoded.value : null;
|
|
570
|
+
}
|
|
480
571
|
function postToNative(message) {
|
|
572
|
+
const encodedMessage = encodeForNative(message);
|
|
573
|
+
if (!encodedMessage)
|
|
574
|
+
return null;
|
|
481
575
|
const kind = message.kind;
|
|
482
576
|
if (kind === "req" || kind === "notify")
|
|
483
577
|
log(`postToNative: ${kind} ${message.method}`);
|
|
484
578
|
if (isDebugEnabled("proto"))
|
|
485
|
-
|
|
579
|
+
// Log the payload before RequiredV3 adds its document secret. The bound
|
|
580
|
+
// console wrapper posts what it prints, which would re-enter this function.
|
|
581
|
+
nativeConsole.log("→", JSON.stringify(message, null, 2));
|
|
486
582
|
try {
|
|
583
|
+
const messageString = stringifyForNative(encodedMessage);
|
|
584
|
+
const bytes = utf8ByteLength(messageString);
|
|
585
|
+
if (bytes > MAX_NATIVE_MESSAGE_BYTES) {
|
|
586
|
+
return {
|
|
587
|
+
code: BRIDGE_ERROR.MESSAGE_TOO_LARGE,
|
|
588
|
+
message: `Bridge message of ${bytes} bytes exceeds the ${MAX_NATIVE_MESSAGE_BYTES}-byte native limit`,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
487
591
|
if (communicationMethod === "webkit") {
|
|
488
|
-
window.webkit?.messageHandlers[NATIVE_HANDLER_NAME]?.postMessage(
|
|
489
|
-
return;
|
|
592
|
+
window.webkit?.messageHandlers[NATIVE_HANDLER_NAME]?.postMessage(messageString);
|
|
593
|
+
return null;
|
|
490
594
|
}
|
|
491
|
-
const messageString = stringifyForNative(message);
|
|
492
595
|
if (communicationMethod === MESSAGE_PORT_TYPE && messagePort) {
|
|
493
596
|
messagePort.postMessage(messageString);
|
|
494
|
-
return;
|
|
597
|
+
return null;
|
|
495
598
|
}
|
|
496
599
|
if ((communicationMethod === JS_INTERFACE_TYPE ||
|
|
497
600
|
communicationMethod === WEB_MESSAGE_TYPE) &&
|
|
498
601
|
window.LingXiaProxy?.postMessage) {
|
|
499
602
|
window.LingXiaProxy.postMessage(messageString);
|
|
500
|
-
return;
|
|
603
|
+
return null;
|
|
501
604
|
}
|
|
502
605
|
warn("Transport not ready");
|
|
503
606
|
}
|
|
504
607
|
catch (e) {
|
|
505
|
-
|
|
608
|
+
if (protocolMode.kind === "required-v3")
|
|
609
|
+
error("Send error");
|
|
610
|
+
else
|
|
611
|
+
error("Send error:", e);
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
function formatBoundConsoleMessage(args) {
|
|
616
|
+
const message = args
|
|
617
|
+
.map((arg) => {
|
|
618
|
+
if (typeof arg === "string")
|
|
619
|
+
return arg;
|
|
620
|
+
try {
|
|
621
|
+
const encoded = stringifyForNative(arg);
|
|
622
|
+
return typeof encoded === "string" ? encoded : String(arg);
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
return "[Unserializable]";
|
|
626
|
+
}
|
|
627
|
+
})
|
|
628
|
+
.join(" ");
|
|
629
|
+
return message.length <= BOUND_CONSOLE_MESSAGE_CHARS
|
|
630
|
+
? message
|
|
631
|
+
: `${message.slice(0, BOUND_CONSOLE_MESSAGE_CHARS)}…`;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* BrowserControl console is an authenticated auxiliary V3 frame. Platform
|
|
635
|
+
* console hooks are disabled for BrowserRelaxed WebViews, so this is the only
|
|
636
|
+
* path that can reach the native log delegate for a control document.
|
|
637
|
+
*/
|
|
638
|
+
function installBoundV3Console() {
|
|
639
|
+
if (protocolMode.kind !== "required-v3")
|
|
640
|
+
return;
|
|
641
|
+
const methods = ["log", "error", "warn", "info", "debug"];
|
|
642
|
+
for (const level of methods) {
|
|
643
|
+
const original = nativeConsole[level];
|
|
644
|
+
console[level] = (...args) => {
|
|
645
|
+
original(...args);
|
|
646
|
+
// Native repeats Active session, WebMessageContext, generation and rate
|
|
647
|
+
// checks. Avoid sending pre-handshake output that can never be admitted.
|
|
648
|
+
if (!handshakeDone)
|
|
649
|
+
return;
|
|
650
|
+
postToNative({
|
|
651
|
+
kind: "console",
|
|
652
|
+
__lingxia_console__: true,
|
|
653
|
+
level,
|
|
654
|
+
message: formatBoundConsoleMessage(args),
|
|
655
|
+
});
|
|
656
|
+
};
|
|
506
657
|
}
|
|
507
658
|
}
|
|
508
659
|
// Handshake state
|
|
509
660
|
let handshakeSessionId = null;
|
|
661
|
+
let helloAcknowledged = false;
|
|
510
662
|
let handshakeDone = false;
|
|
511
663
|
let helloSent = false;
|
|
512
664
|
let handshakeRetryCount = 0;
|
|
513
665
|
let handshakeTimer = null;
|
|
514
|
-
// Host method schema — populated from handshake `Ready` message.
|
|
515
|
-
// Maps "namespace.method" → "call" | "stream".
|
|
516
|
-
const hostMethodKinds = {};
|
|
517
666
|
// Request tracking
|
|
518
667
|
let requestCounter = 0;
|
|
519
668
|
function createListenerBuckets() {
|
|
@@ -894,6 +1043,26 @@ function removeOutboxByReqId(reqId) {
|
|
|
894
1043
|
}
|
|
895
1044
|
return false;
|
|
896
1045
|
}
|
|
1046
|
+
// UTF-8 byte length without allocating a copy of the string.
|
|
1047
|
+
function utf8ByteLength(value) {
|
|
1048
|
+
if (typeof TextEncoder === "function")
|
|
1049
|
+
return new TextEncoder().encode(value).length;
|
|
1050
|
+
let bytes = 0;
|
|
1051
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1052
|
+
const code = value.charCodeAt(i);
|
|
1053
|
+
if (code < 0x80)
|
|
1054
|
+
bytes += 1;
|
|
1055
|
+
else if (code < 0x800)
|
|
1056
|
+
bytes += 2;
|
|
1057
|
+
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
1058
|
+
bytes += 4;
|
|
1059
|
+
i += 1;
|
|
1060
|
+
}
|
|
1061
|
+
else
|
|
1062
|
+
bytes += 3;
|
|
1063
|
+
}
|
|
1064
|
+
return bytes;
|
|
1065
|
+
}
|
|
897
1066
|
function send(msg, reqId) {
|
|
898
1067
|
const kind = msg.kind;
|
|
899
1068
|
const isHandshake = kind === "hello" || kind === "helloAck" || kind === "ready";
|
|
@@ -913,7 +1082,16 @@ function send(msg, reqId) {
|
|
|
913
1082
|
}
|
|
914
1083
|
if (reqId)
|
|
915
1084
|
armPendingOperationTimer(reqId);
|
|
916
|
-
postToNative(msg);
|
|
1085
|
+
reportPostFailure(postToNative(msg), reqId);
|
|
1086
|
+
}
|
|
1087
|
+
// An oversized frame is dropped by native without a reply, so the caller would
|
|
1088
|
+
// otherwise learn about it only when its request timed out.
|
|
1089
|
+
function reportPostFailure(failure, reqId) {
|
|
1090
|
+
if (!failure)
|
|
1091
|
+
return;
|
|
1092
|
+
error(failure.message);
|
|
1093
|
+
if (reqId)
|
|
1094
|
+
rejectPendingOperation(reqId, failure);
|
|
917
1095
|
}
|
|
918
1096
|
function flushOutbox() {
|
|
919
1097
|
if (!canSendAppMessages())
|
|
@@ -924,7 +1102,7 @@ function flushOutbox() {
|
|
|
924
1102
|
continue;
|
|
925
1103
|
if (item.reqId)
|
|
926
1104
|
armPendingOperationTimer(item.reqId);
|
|
927
|
-
postToNative(item.msg);
|
|
1105
|
+
reportPostFailure(postToNative(item.msg), item.reqId);
|
|
928
1106
|
}
|
|
929
1107
|
}
|
|
930
1108
|
function clearHandshakeTimer() {
|
|
@@ -936,6 +1114,10 @@ function clearHandshakeTimer() {
|
|
|
936
1114
|
function startHandshake() {
|
|
937
1115
|
if (handshakeDone)
|
|
938
1116
|
return;
|
|
1117
|
+
if (!protocolInitialized)
|
|
1118
|
+
return;
|
|
1119
|
+
if (protocolMode.kind === "blocked")
|
|
1120
|
+
return;
|
|
939
1121
|
if (!isTransportReady())
|
|
940
1122
|
return;
|
|
941
1123
|
clearHandshakeTimer();
|
|
@@ -944,7 +1126,7 @@ function startHandshake() {
|
|
|
944
1126
|
kind: "hello",
|
|
945
1127
|
nonce: BRIDGE_CONFIG.nonce || "",
|
|
946
1128
|
role: "view",
|
|
947
|
-
protocolsSupported: [2],
|
|
1129
|
+
protocolsSupported: protocolMode.kind === "required-v3" ? [3] : [2],
|
|
948
1130
|
};
|
|
949
1131
|
helloSent = true;
|
|
950
1132
|
postToNative(hello);
|
|
@@ -981,6 +1163,27 @@ function startHandshake() {
|
|
|
981
1163
|
}, HANDSHAKE_TIMEOUT_MS);
|
|
982
1164
|
}
|
|
983
1165
|
function parseIncoming(msg) {
|
|
1166
|
+
if (protocolMode.kind === "blocked")
|
|
1167
|
+
return null;
|
|
1168
|
+
if (protocolMode.kind === "required-v3") {
|
|
1169
|
+
const frame = typeof msg === "string" ? msg : stringifyForNative(msg);
|
|
1170
|
+
const parsed = protocolMode.codec.parse(frame);
|
|
1171
|
+
if (!parsed.ok)
|
|
1172
|
+
return null;
|
|
1173
|
+
return {
|
|
1174
|
+
v: 3,
|
|
1175
|
+
kind: parsed.value.kind,
|
|
1176
|
+
...parsed.value.payload,
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
if (typeof msg === "string") {
|
|
1180
|
+
try {
|
|
1181
|
+
msg = JSON.parse(msg);
|
|
1182
|
+
}
|
|
1183
|
+
catch {
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
984
1187
|
if (!msg || typeof msg !== "object")
|
|
985
1188
|
return null;
|
|
986
1189
|
const v = msg.v;
|
|
@@ -1106,17 +1309,26 @@ function applySnapshotFromResult(result) {
|
|
|
1106
1309
|
pageData = obj.state;
|
|
1107
1310
|
stateRev = obj.rev;
|
|
1108
1311
|
if (isDebugEnabled("data")) {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1312
|
+
nativeConsole.group("[LX] snapshot(res)");
|
|
1313
|
+
nativeConsole.log("rev:", stateRev, "state:", deepCopy(pageData));
|
|
1314
|
+
nativeConsole.groupEnd();
|
|
1112
1315
|
}
|
|
1113
1316
|
notifyStateSubscribers(true);
|
|
1114
1317
|
return true;
|
|
1115
1318
|
}
|
|
1116
1319
|
function handleIncomingMessage(msg) {
|
|
1320
|
+
let candidate = msg;
|
|
1321
|
+
if (protocolMode.kind === "v2" && typeof candidate === "string") {
|
|
1322
|
+
try {
|
|
1323
|
+
candidate = JSON.parse(candidate);
|
|
1324
|
+
}
|
|
1325
|
+
catch {
|
|
1326
|
+
candidate = null;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1117
1329
|
// Handle native component events (from Android NativeBridge.sendEventToView)
|
|
1118
|
-
if (
|
|
1119
|
-
const obj =
|
|
1330
|
+
if (protocolMode.kind === "v2" && candidate && typeof candidate === "object") {
|
|
1331
|
+
const obj = candidate;
|
|
1120
1332
|
if (obj.type === "event" && obj.name === "nativecomponent" && obj.payload) {
|
|
1121
1333
|
const payload = obj.payload;
|
|
1122
1334
|
const componentId = payload.id ||
|
|
@@ -1135,30 +1347,46 @@ function handleIncomingMessage(msg) {
|
|
|
1135
1347
|
return;
|
|
1136
1348
|
}
|
|
1137
1349
|
}
|
|
1138
|
-
const message = parseIncoming(
|
|
1350
|
+
const message = parseIncoming(candidate);
|
|
1139
1351
|
if (!message) {
|
|
1140
|
-
warn("Invalid
|
|
1352
|
+
warn("Invalid bridge message");
|
|
1141
1353
|
return;
|
|
1142
1354
|
}
|
|
1143
1355
|
switch (message.kind) {
|
|
1144
1356
|
case "helloAck":
|
|
1145
|
-
|
|
1357
|
+
if (message.protocol !== (protocolMode.kind === "required-v3" ? 3 : 2) ||
|
|
1358
|
+
message.nonce !== (BRIDGE_CONFIG.nonce || "")) {
|
|
1359
|
+
warn("Invalid helloAck");
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
if (protocolMode.kind === "required-v3") {
|
|
1363
|
+
helloAcknowledged = true;
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
if (handshakeSessionId && message.sessionId !== handshakeSessionId) {
|
|
1367
|
+
warn("sessionId mismatch");
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
if (!handshakeSessionId)
|
|
1371
|
+
handshakeSessionId = message.sessionId;
|
|
1372
|
+
helloAcknowledged = true;
|
|
1146
1373
|
return;
|
|
1147
1374
|
case "ready":
|
|
1148
|
-
if (
|
|
1375
|
+
if (!helloAcknowledged)
|
|
1376
|
+
return;
|
|
1377
|
+
if (protocolMode.kind === "v2" &&
|
|
1378
|
+
handshakeSessionId &&
|
|
1379
|
+
message.sessionId !== handshakeSessionId) {
|
|
1149
1380
|
warn("sessionId mismatch");
|
|
1150
1381
|
return;
|
|
1151
1382
|
}
|
|
1152
1383
|
clearHandshakeTimer();
|
|
1153
1384
|
handshakeDone = true;
|
|
1154
1385
|
handshakeRetryCount = 0;
|
|
1155
|
-
if (
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
}
|
|
1386
|
+
if (isDebugEnabled("proto")) {
|
|
1387
|
+
log("Handshake complete, host routes:", Object.keys(message.hostMethods || {}).length +
|
|
1388
|
+
(message.hostChannels || []).length);
|
|
1159
1389
|
}
|
|
1160
|
-
if (isDebugEnabled("proto"))
|
|
1161
|
-
log("Handshake complete, hostMethods:", Object.keys(hostMethodKinds).length);
|
|
1162
1390
|
flushOutbox();
|
|
1163
1391
|
return;
|
|
1164
1392
|
case "res": {
|
|
@@ -1216,9 +1444,9 @@ function handleIncomingMessage(msg) {
|
|
|
1216
1444
|
pageData = message.state || {};
|
|
1217
1445
|
stateRev = message.rev;
|
|
1218
1446
|
if (isDebugEnabled("data")) {
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1447
|
+
nativeConsole.group("[LX] snapshot");
|
|
1448
|
+
nativeConsole.log("rev:", stateRev, "state:", deepCopy(pageData));
|
|
1449
|
+
nativeConsole.groupEnd();
|
|
1222
1450
|
}
|
|
1223
1451
|
notifyStateSubscribers(true);
|
|
1224
1452
|
return;
|
|
@@ -1238,9 +1466,9 @@ function handleIncomingMessage(msg) {
|
|
|
1238
1466
|
return;
|
|
1239
1467
|
}
|
|
1240
1468
|
if (isDebugEnabled("data")) {
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1469
|
+
nativeConsole.group("[LX] patch");
|
|
1470
|
+
nativeConsole.log("rev:", stateRev, "ops:", message.ops);
|
|
1471
|
+
nativeConsole.groupEnd();
|
|
1244
1472
|
}
|
|
1245
1473
|
notifyStateSubscribers(false);
|
|
1246
1474
|
if (message.ack)
|
|
@@ -1349,7 +1577,8 @@ function hasNativeComponentHandler() {
|
|
|
1349
1577
|
if (typeof window === "undefined")
|
|
1350
1578
|
return false;
|
|
1351
1579
|
return !!(window.webkit?.messageHandlers?.NativeComponent ||
|
|
1352
|
-
window.NativeComponentBridge?.postMessage
|
|
1580
|
+
window.NativeComponentBridge?.postMessage ||
|
|
1581
|
+
window.LingXiaProxy?.nativeComponentUpdate);
|
|
1353
1582
|
}
|
|
1354
1583
|
function postNativeComponentMessage(message) {
|
|
1355
1584
|
try {
|
|
@@ -1361,6 +1590,18 @@ function postNativeComponentMessage(message) {
|
|
|
1361
1590
|
window.NativeComponentBridge.postMessage(stringifyForNative(message));
|
|
1362
1591
|
return;
|
|
1363
1592
|
}
|
|
1593
|
+
const update = window.LingXiaProxy?.nativeComponentUpdate;
|
|
1594
|
+
if (typeof update === "function") {
|
|
1595
|
+
const componentId = message.id || "island";
|
|
1596
|
+
const payload = stringifyForNative({ ...message, componentId });
|
|
1597
|
+
try {
|
|
1598
|
+
update.call(window.LingXiaProxy, payload);
|
|
1599
|
+
}
|
|
1600
|
+
catch {
|
|
1601
|
+
update.call(window.LingXiaProxy, componentId, payload);
|
|
1602
|
+
}
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1364
1605
|
}
|
|
1365
1606
|
catch (e) {
|
|
1366
1607
|
error("NativeComponent send error:", e);
|
|
@@ -1594,16 +1835,7 @@ export const LingXiaBridge = {
|
|
|
1594
1835
|
}
|
|
1595
1836
|
messagePort = port;
|
|
1596
1837
|
port.onmessage = (event) => {
|
|
1597
|
-
|
|
1598
|
-
if (typeof data === "string") {
|
|
1599
|
-
try {
|
|
1600
|
-
data = JSON.parse(data);
|
|
1601
|
-
}
|
|
1602
|
-
catch {
|
|
1603
|
-
return;
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
handleIncomingMessage(data);
|
|
1838
|
+
handleIncomingMessage(event.data);
|
|
1607
1839
|
};
|
|
1608
1840
|
// Some WebView MessagePort implementations (notably Android WebMessagePort)
|
|
1609
1841
|
// require an explicit start() to begin dispatching onmessage events.
|
|
@@ -1615,15 +1847,16 @@ export const LingXiaBridge = {
|
|
|
1615
1847
|
startHandshake();
|
|
1616
1848
|
},
|
|
1617
1849
|
_receiveEvaluateMessage(messageString) {
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
handleIncomingMessage(JSON.parse(messageString));
|
|
1621
|
-
}
|
|
1622
|
-
catch (e) {
|
|
1623
|
-
error("Parse error:", e);
|
|
1624
|
-
}
|
|
1850
|
+
if (messageString)
|
|
1851
|
+
handleIncomingMessage(messageString);
|
|
1625
1852
|
},
|
|
1626
1853
|
debug: createDebugObject(debugFlags),
|
|
1854
|
+
// The host language, for documents that cannot bundle `@lingxia/bridge` —
|
|
1855
|
+
// browser internal pages and plain-HTML lxapps load this runtime as a script.
|
|
1856
|
+
displayLanguage: {
|
|
1857
|
+
get: getDisplayLanguage,
|
|
1858
|
+
subscribe: subscribeDisplayLanguage,
|
|
1859
|
+
},
|
|
1627
1860
|
platform: {
|
|
1628
1861
|
isHarmony,
|
|
1629
1862
|
isIOS,
|
|
@@ -1631,6 +1864,7 @@ export const LingXiaBridge = {
|
|
|
1631
1864
|
isMacOS,
|
|
1632
1865
|
isWindows,
|
|
1633
1866
|
isDesktop,
|
|
1867
|
+
isMobile,
|
|
1634
1868
|
isApple,
|
|
1635
1869
|
isRunner,
|
|
1636
1870
|
getOS: getPlatformOS,
|
|
@@ -1769,6 +2003,15 @@ export function initBridge() {
|
|
|
1769
2003
|
}
|
|
1770
2004
|
window.__LX_BRIDGE_INIT_STATE = "initializing";
|
|
1771
2005
|
try {
|
|
2006
|
+
if (!protocolInitialized) {
|
|
2007
|
+
const activation = consumeV3Bootstrap();
|
|
2008
|
+
protocolMode =
|
|
2009
|
+
activation.kind === "required"
|
|
2010
|
+
? { kind: "required-v3", codec: activation.codec }
|
|
2011
|
+
: { kind: activation.kind === "absent" ? "v2" : "blocked" };
|
|
2012
|
+
protocolInitialized = true;
|
|
2013
|
+
}
|
|
2014
|
+
installBoundV3Console();
|
|
1772
2015
|
log(`Method: ${communicationMethod}`);
|
|
1773
2016
|
activateReceiver(LingXiaBridge._receiveEvaluateMessage);
|
|
1774
2017
|
if (useAppleDownstreamTransport()) {
|
|
@@ -1778,7 +2021,15 @@ export function initBridge() {
|
|
|
1778
2021
|
}
|
|
1779
2022
|
else if (communicationMethod === MESSAGE_PORT_TYPE) {
|
|
1780
2023
|
installMessagePortInitListener();
|
|
1781
|
-
|
|
2024
|
+
const tryPort = (attempt) => {
|
|
2025
|
+
getMessagePort().catch((e) => {
|
|
2026
|
+
warn("Port init failed:", e);
|
|
2027
|
+
if (attempt < 3) {
|
|
2028
|
+
window.setTimeout(() => tryPort(attempt + 1), 500 * attempt);
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
};
|
|
2032
|
+
tryPort(1);
|
|
1782
2033
|
}
|
|
1783
2034
|
else if (communicationMethod === "webkit" ||
|
|
1784
2035
|
communicationMethod === JS_INTERFACE_TYPE ||
|