@lingxia/bridge 0.15.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.
@@ -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 { BRIDGE_CONFIG, getCommunicationMethod, getPlatformOS, isAndroid, isHarmony, isIOS, isMacOS, isWindows, isDesktop, isMobile, isApple, isDevSession, isRunner, } from "./runtime-env";
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
- console.log(`[LX] ${key}: ${value}`);
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
- if (typeof window[GLOBAL_RECEIVER_NAME] === "function")
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 activateReceiver(receiver) {
50
- if (typeof window === "undefined")
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
- console.log(LOG_PREFIX, ...args);
137
+ nativeConsole.log(LOG_PREFIX, ...args);
81
138
  }
82
139
  function warn(...args) {
83
- console.warn(LOG_PREFIX, ...args);
140
+ nativeConsole.warn(LOG_PREFIX, ...args);
84
141
  }
85
142
  function error(...args) {
86
- console.error(LOG_PREFIX, ...args);
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
- handshakeSessionId = null;
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
- console.log("→", JSON.stringify(message, null, 2));
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(message);
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
- error("Send error:", e);
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
- console.group("[LX] snapshot(res)");
1110
- console.log("rev:", stateRev, "state:", deepCopy(pageData));
1111
- console.groupEnd();
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 (msg && typeof msg === "object") {
1119
- const obj = msg;
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(msg);
1350
+ const message = parseIncoming(candidate);
1139
1351
  if (!message) {
1140
- warn("Invalid V2 message:", msg);
1352
+ warn("Invalid bridge message");
1141
1353
  return;
1142
1354
  }
1143
1355
  switch (message.kind) {
1144
1356
  case "helloAck":
1145
- handshakeSessionId = message.sessionId;
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 (handshakeSessionId && message.sessionId !== handshakeSessionId) {
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 (message.hostMethods) {
1156
- for (const [k, v] of Object.entries(message.hostMethods)) {
1157
- hostMethodKinds[k] = v;
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
- console.group("[LX] snapshot");
1220
- console.log("rev:", stateRev, "state:", deepCopy(pageData));
1221
- console.groupEnd();
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
- console.group("[LX] patch");
1242
- console.log("rev:", stateRev, "ops:", message.ops);
1243
- console.groupEnd();
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
- let data = event.data;
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
- try {
1619
- if (messageString)
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,
@@ -1770,6 +2003,15 @@ export function initBridge() {
1770
2003
  }
1771
2004
  window.__LX_BRIDGE_INIT_STATE = "initializing";
1772
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();
1773
2015
  log(`Method: ${communicationMethod}`);
1774
2016
  activateReceiver(LingXiaBridge._receiveEvaluateMessage);
1775
2017
  if (useAppleDownstreamTransport()) {
@@ -1779,7 +2021,15 @@ export function initBridge() {
1779
2021
  }
1780
2022
  else if (communicationMethod === MESSAGE_PORT_TYPE) {
1781
2023
  installMessagePortInitListener();
1782
- getMessagePort().catch((e) => warn("Port init failed:", e));
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);
1783
2033
  }
1784
2034
  else if (communicationMethod === "webkit" ||
1785
2035
  communicationMethod === JS_INTERFACE_TYPE ||