@lingxia/bridge 0.10.0 → 0.11.1
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 +200 -20
- package/dist/es2020/index.js +1 -0
- package/dist/es2020/runtime-env.js +25 -0
- package/dist/es2020/ui/action-sheet.js +2 -2
- package/dist/es5/bridge.js +205 -25
- package/dist/es5/index.js +1 -0
- package/dist/es5/runtime-env.js +26 -0
- package/dist/es5/ui/action-sheet.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/runtime-env.d.ts +4 -0
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/es2020/bridge.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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, } from "./runtime-env";
|
|
4
|
+
import { BRIDGE_CONFIG, getCommunicationMethod, getPlatformOS, isAndroid, isHarmony, isIOS, isMacOS, isWindows, isDesktop, isApple, isDevSession, isRunner, } from "./runtime-env";
|
|
5
5
|
const NATIVE_HANDLER_NAME = "LingXia";
|
|
6
6
|
const GLOBAL_RECEIVER_NAME = "__LingXiaRecvMessage";
|
|
7
7
|
const DEFAULT_TIMEOUT_MS = 5000;
|
|
@@ -65,7 +65,18 @@ function activateReceiver(receiver) {
|
|
|
65
65
|
function isDebugEnabled(flag) {
|
|
66
66
|
return debugFlags.all || debugFlags[flag];
|
|
67
67
|
}
|
|
68
|
+
// `log` is the bridge's own protocol/lifecycle trace. Native log capture
|
|
69
|
+
// forwards whatever the page emits to `console`, so the bridge itself decides
|
|
70
|
+
// whether to surface this framework chatter: only in a `lingxia dev` session
|
|
71
|
+
// (or when a debug flag is set). Shipped apps stay quiet, leaving the captured
|
|
72
|
+
// stream to the page's own output plus bridge warnings/errors.
|
|
68
73
|
function log(...args) {
|
|
74
|
+
if (!isDevSession() &&
|
|
75
|
+
!debugFlags.all &&
|
|
76
|
+
!debugFlags.proto &&
|
|
77
|
+
!debugFlags.data) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
69
80
|
console.log(LOG_PREFIX, ...args);
|
|
70
81
|
}
|
|
71
82
|
function warn(...args) {
|
|
@@ -112,6 +123,17 @@ let appleDownstreamTask = null;
|
|
|
112
123
|
let appleDownstreamAbortController = null;
|
|
113
124
|
let appleReconnectTimer = null;
|
|
114
125
|
let appleReconnectDelayMs = APPLE_RECONNECT_BASE_MS;
|
|
126
|
+
let appleDownstreamDisposed = false;
|
|
127
|
+
// Highest transport frame seq processed. Sent as `?from=` on reconnect so the
|
|
128
|
+
// host replays the gap; survives reconnects so a WebKit-replaced stream resumes
|
|
129
|
+
// without losing frames or tearing down the bridge session.
|
|
130
|
+
let appleLastFrameSeq = 0;
|
|
131
|
+
// Liveness tracking: the host heartbeats an idle connection, so a gap longer
|
|
132
|
+
// than a couple of heartbeats means the connection is silently dead and we
|
|
133
|
+
// reconnect proactively instead of waiting for a read error that may never come.
|
|
134
|
+
const APPLE_DOWNSTREAM_STALE_MS = 35000;
|
|
135
|
+
let appleLastFrameAt = 0;
|
|
136
|
+
let appleWatchdogTimer = null;
|
|
115
137
|
const portInitState = {
|
|
116
138
|
listenerInstalled: false,
|
|
117
139
|
promise: null,
|
|
@@ -155,6 +177,23 @@ function clearAppleReconnectTimer() {
|
|
|
155
177
|
appleReconnectTimer = null;
|
|
156
178
|
}
|
|
157
179
|
}
|
|
180
|
+
function disposeAppleDownstream(reason) {
|
|
181
|
+
if (!useAppleDownstreamTransport() || appleDownstreamDisposed)
|
|
182
|
+
return;
|
|
183
|
+
appleDownstreamDisposed = true;
|
|
184
|
+
clearAppleReconnectTimer();
|
|
185
|
+
if (appleWatchdogTimer !== null) {
|
|
186
|
+
clearInterval(appleWatchdogTimer);
|
|
187
|
+
appleWatchdogTimer = null;
|
|
188
|
+
}
|
|
189
|
+
if (typeof document !== "undefined") {
|
|
190
|
+
document.removeEventListener("visibilitychange", handleAppleVisibilityChange);
|
|
191
|
+
}
|
|
192
|
+
appleForcedReconnectPending = false;
|
|
193
|
+
appleDownstreamAbortController?.abort();
|
|
194
|
+
appleDownstreamConnected = false;
|
|
195
|
+
resetHandshakeState(reason, true);
|
|
196
|
+
}
|
|
158
197
|
function closeActiveChannelsFromTransport(reason) {
|
|
159
198
|
for (const [id, channel] of Array.from(activeChannels.entries())) {
|
|
160
199
|
activeChannels.delete(id);
|
|
@@ -196,7 +235,7 @@ function processAppleDownstreamBuffer(buffer) {
|
|
|
196
235
|
if (!line)
|
|
197
236
|
continue;
|
|
198
237
|
try {
|
|
199
|
-
|
|
238
|
+
handleAppleDownstreamFrame(JSON.parse(line));
|
|
200
239
|
}
|
|
201
240
|
catch (e) {
|
|
202
241
|
warn("Apple downstream parse error:", e, line);
|
|
@@ -204,13 +243,49 @@ function processAppleDownstreamBuffer(buffer) {
|
|
|
204
243
|
}
|
|
205
244
|
return remaining;
|
|
206
245
|
}
|
|
246
|
+
// Unwrap the transport envelope: {"lxff":seq,"m":<message>} carries a business
|
|
247
|
+
// message with its frame seq; {"lxreset":true} means the host cannot replay our
|
|
248
|
+
// resume point and we must re-handshake.
|
|
249
|
+
function handleAppleDownstreamFrame(frame) {
|
|
250
|
+
if (!frame || typeof frame !== "object")
|
|
251
|
+
return;
|
|
252
|
+
// Any frame — data, heartbeat, or reset — proves the connection is alive.
|
|
253
|
+
appleLastFrameAt = Date.now();
|
|
254
|
+
const record = frame;
|
|
255
|
+
if (record.lxshutdown === true) {
|
|
256
|
+
disposeAppleDownstream("Apple downstream closed");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (record.lxhb !== undefined)
|
|
260
|
+
return; // heartbeat: liveness only, nothing to dispatch
|
|
261
|
+
if (record.lxreset === true) {
|
|
262
|
+
appleLastFrameSeq = 0;
|
|
263
|
+
resetHandshakeState("Apple downstream reset", true);
|
|
264
|
+
startHandshake();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (typeof record.lxff === "number") {
|
|
268
|
+
// Replayed frames after a reconnect can repeat the last-seen seq; ignore
|
|
269
|
+
// anything we have already processed so the session is not double-fed.
|
|
270
|
+
if (record.lxff <= appleLastFrameSeq)
|
|
271
|
+
return;
|
|
272
|
+
appleLastFrameSeq = record.lxff;
|
|
273
|
+
handleIncomingMessage(record.m);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// Legacy/un-enveloped line (e.g. a bare keepalive); pass through.
|
|
277
|
+
handleIncomingMessage(frame);
|
|
278
|
+
}
|
|
207
279
|
async function runAppleDownstream() {
|
|
208
280
|
if (!APPLE_DOWNSTREAM_URL) {
|
|
209
281
|
throw new Error("Apple downstream URL is not configured");
|
|
210
282
|
}
|
|
211
283
|
const controller = new AbortController();
|
|
212
284
|
appleDownstreamAbortController = controller;
|
|
213
|
-
|
|
285
|
+
// Resume from the last frame we saw so the host replays the gap on reconnect.
|
|
286
|
+
const separator = APPLE_DOWNSTREAM_URL.includes("?") ? "&" : "?";
|
|
287
|
+
const url = `${APPLE_DOWNSTREAM_URL}${separator}from=${appleLastFrameSeq}`;
|
|
288
|
+
const response = await fetch(url, {
|
|
214
289
|
method: "GET",
|
|
215
290
|
cache: "no-store",
|
|
216
291
|
headers: { Accept: "application/x-ndjson" },
|
|
@@ -219,11 +294,16 @@ async function runAppleDownstream() {
|
|
|
219
294
|
if (!response.ok) {
|
|
220
295
|
throw new Error(`Apple downstream HTTP ${response.status}`);
|
|
221
296
|
}
|
|
297
|
+
if (response.headers.get("X-LingXia-Bridge-Shutdown") === "1") {
|
|
298
|
+
disposeAppleDownstream("Apple downstream closed");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
222
301
|
if (!response.body) {
|
|
223
302
|
throw new Error("Apple downstream response body unavailable");
|
|
224
303
|
}
|
|
225
304
|
appleDownstreamConnected = true;
|
|
226
305
|
appleReconnectDelayMs = APPLE_RECONNECT_BASE_MS;
|
|
306
|
+
appleLastFrameAt = Date.now();
|
|
227
307
|
if (isDebugEnabled("proto"))
|
|
228
308
|
log("Apple downstream connected");
|
|
229
309
|
startHandshake();
|
|
@@ -242,7 +322,7 @@ async function runAppleDownstream() {
|
|
|
242
322
|
const tail = buffered.trim();
|
|
243
323
|
if (tail) {
|
|
244
324
|
try {
|
|
245
|
-
|
|
325
|
+
handleAppleDownstreamFrame(JSON.parse(tail));
|
|
246
326
|
}
|
|
247
327
|
catch (e) {
|
|
248
328
|
warn("Apple downstream trailing parse error:", e, tail);
|
|
@@ -261,13 +341,17 @@ async function runAppleDownstream() {
|
|
|
261
341
|
catch { }
|
|
262
342
|
}
|
|
263
343
|
}
|
|
264
|
-
function scheduleAppleDownstreamReconnect(reason) {
|
|
265
|
-
if (!useAppleDownstreamTransport())
|
|
344
|
+
function scheduleAppleDownstreamReconnect(reason, immediate = false) {
|
|
345
|
+
if (!useAppleDownstreamTransport() || appleDownstreamDisposed)
|
|
266
346
|
return;
|
|
267
347
|
clearAppleReconnectTimer();
|
|
268
|
-
const delay = appleReconnectDelayMs;
|
|
269
|
-
|
|
270
|
-
|
|
348
|
+
const delay = immediate ? 0 : appleReconnectDelayMs;
|
|
349
|
+
if (!immediate) {
|
|
350
|
+
appleReconnectDelayMs = Math.min(appleReconnectDelayMs * 2, APPLE_RECONNECT_MAX_MS);
|
|
351
|
+
}
|
|
352
|
+
const message = immediate
|
|
353
|
+
? `Apple downstream completed, reconnecting immediately: ${reason}`
|
|
354
|
+
: `Apple downstream disconnected, retrying in ${delay}ms: ${reason}`;
|
|
271
355
|
if (delay <= APPLE_RECONNECT_BASE_MS) {
|
|
272
356
|
log(message);
|
|
273
357
|
}
|
|
@@ -280,12 +364,16 @@ function scheduleAppleDownstreamReconnect(reason) {
|
|
|
280
364
|
}, delay);
|
|
281
365
|
}
|
|
282
366
|
function ensureAppleDownstream() {
|
|
283
|
-
if (!useAppleDownstreamTransport())
|
|
367
|
+
if (!useAppleDownstreamTransport() || appleDownstreamDisposed)
|
|
284
368
|
return;
|
|
285
369
|
if (appleDownstreamTask)
|
|
286
370
|
return;
|
|
287
371
|
clearAppleReconnectTimer();
|
|
372
|
+
let completedCleanly = false;
|
|
288
373
|
appleDownstreamTask = runAppleDownstream()
|
|
374
|
+
.then(() => {
|
|
375
|
+
completedCleanly = true;
|
|
376
|
+
})
|
|
289
377
|
.catch((e) => {
|
|
290
378
|
if (appleDownstreamAbortController?.signal.aborted)
|
|
291
379
|
return;
|
|
@@ -295,13 +383,72 @@ function ensureAppleDownstream() {
|
|
|
295
383
|
const aborted = appleDownstreamAbortController?.signal.aborted ?? false;
|
|
296
384
|
appleDownstreamTask = null;
|
|
297
385
|
appleDownstreamAbortController = null;
|
|
298
|
-
const wasConnected = appleDownstreamConnected;
|
|
299
386
|
appleDownstreamConnected = false;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
387
|
+
// A dropped transport is not a dead session: reconnect resumes from the
|
|
388
|
+
// last seq and the host replays the gap, so the handshake and in-flight
|
|
389
|
+
// streams stay intact. Only a host-sent reset (handled on the frame path)
|
|
390
|
+
// or a real abort tears things down.
|
|
391
|
+
if (!aborted && !appleDownstreamDisposed) {
|
|
392
|
+
// Native intentionally completes bootstrap/replay responses to flush
|
|
393
|
+
// WebKit. Reconnect without the failure backoff so a live stream can
|
|
394
|
+
// catch up and settle onto a long-lived downstream.
|
|
395
|
+
scheduleAppleDownstreamReconnect("stream closed", completedCleanly);
|
|
396
|
+
}
|
|
303
397
|
});
|
|
304
398
|
}
|
|
399
|
+
// Drop the current downstream and reconnect, exactly as WebKit does when it
|
|
400
|
+
// replaces the streaming fetch. The reconnect carries the real `from=<lastSeq>`,
|
|
401
|
+
// so nothing is lost. Driven by the staleness watchdog and the foreground
|
|
402
|
+
// handler, and exposed as a dev hook for repro harnesses. A no-op off Apple.
|
|
403
|
+
let appleForcedReconnectPending = false;
|
|
404
|
+
function forceDownstreamReconnect() {
|
|
405
|
+
if (!useAppleDownstreamTransport() || appleDownstreamDisposed)
|
|
406
|
+
return;
|
|
407
|
+
// Coalesce a burst of triggers (rapid foreground toggles, repeated calls)
|
|
408
|
+
// into a single reconnect so they cannot stack concurrent fetches.
|
|
409
|
+
if (appleForcedReconnectPending)
|
|
410
|
+
return;
|
|
411
|
+
appleForcedReconnectPending = true;
|
|
412
|
+
const task = appleDownstreamTask;
|
|
413
|
+
appleDownstreamAbortController?.abort();
|
|
414
|
+
const reconnect = () => {
|
|
415
|
+
appleForcedReconnectPending = false;
|
|
416
|
+
ensureAppleDownstream();
|
|
417
|
+
};
|
|
418
|
+
if (task)
|
|
419
|
+
task.finally(reconnect);
|
|
420
|
+
else
|
|
421
|
+
reconnect();
|
|
422
|
+
}
|
|
423
|
+
// A silently dead connection (half-open socket) never surfaces a read error, so
|
|
424
|
+
// poll for heartbeat staleness and reconnect when the host has gone quiet.
|
|
425
|
+
function startAppleWatchdog() {
|
|
426
|
+
if (!useAppleDownstreamTransport() ||
|
|
427
|
+
appleDownstreamDisposed ||
|
|
428
|
+
appleWatchdogTimer !== null)
|
|
429
|
+
return;
|
|
430
|
+
appleWatchdogTimer = setInterval(() => {
|
|
431
|
+
// No `connected` guard: a reconnect that hung before delivering a frame
|
|
432
|
+
// also goes stale, and forcing a reconnect aborts and retries it.
|
|
433
|
+
if (appleLastFrameAt === 0)
|
|
434
|
+
return;
|
|
435
|
+
if (Date.now() - appleLastFrameAt > APPLE_DOWNSTREAM_STALE_MS) {
|
|
436
|
+
warn("Apple downstream stale (no heartbeat), forcing reconnect");
|
|
437
|
+
forceDownstreamReconnect();
|
|
438
|
+
}
|
|
439
|
+
}, 5000);
|
|
440
|
+
}
|
|
441
|
+
// WebKit suspends/throttles a backgrounded webview and may tear the stream
|
|
442
|
+
// down; reconnect the moment it returns so the UI is never left stale.
|
|
443
|
+
function handleAppleVisibilityChange() {
|
|
444
|
+
if (document.visibilityState === "visible")
|
|
445
|
+
forceDownstreamReconnect();
|
|
446
|
+
}
|
|
447
|
+
function installAppleForegroundReconnect() {
|
|
448
|
+
if (!useAppleDownstreamTransport() || typeof document === "undefined")
|
|
449
|
+
return;
|
|
450
|
+
document.addEventListener("visibilitychange", handleAppleVisibilityChange);
|
|
451
|
+
}
|
|
305
452
|
function getMessagePort() {
|
|
306
453
|
if (messagePort)
|
|
307
454
|
return Promise.resolve(messagePort);
|
|
@@ -380,6 +527,9 @@ function createListenerBuckets() {
|
|
|
380
527
|
function createStreamHandle(id, cancelFn) {
|
|
381
528
|
const listeners = createListenerBuckets();
|
|
382
529
|
let done = false;
|
|
530
|
+
// Only buffer for the async iterator; long-lived streams consumed via
|
|
531
|
+
// .on("data") would otherwise accumulate payloads forever.
|
|
532
|
+
let iteratorRequested = false;
|
|
383
533
|
let resolveResult = () => { };
|
|
384
534
|
let rejectResult = () => { };
|
|
385
535
|
const pendingData = [];
|
|
@@ -401,6 +551,7 @@ function createStreamHandle(id, cancelFn) {
|
|
|
401
551
|
return this;
|
|
402
552
|
},
|
|
403
553
|
[Symbol.asyncIterator]() {
|
|
554
|
+
iteratorRequested = true;
|
|
404
555
|
return {
|
|
405
556
|
next() {
|
|
406
557
|
if (pendingData.length > 0) {
|
|
@@ -429,7 +580,7 @@ function createStreamHandle(id, cancelFn) {
|
|
|
429
580
|
if (pendingReads.length > 0) {
|
|
430
581
|
pendingReads.shift().resolve({ done: false, value: payload });
|
|
431
582
|
}
|
|
432
|
-
else {
|
|
583
|
+
else if (iteratorRequested) {
|
|
433
584
|
pendingData.push(payload);
|
|
434
585
|
}
|
|
435
586
|
for (const listener of listeners.data) {
|
|
@@ -483,6 +634,9 @@ function createChannel(id, sendFn, closeFn) {
|
|
|
483
634
|
let open = false;
|
|
484
635
|
let outboundSeq = 0;
|
|
485
636
|
let closed = false;
|
|
637
|
+
// Only buffer for the async iterator or pre-first-listener replay;
|
|
638
|
+
// otherwise a listener-driven channel would accumulate payloads forever.
|
|
639
|
+
let iteratorRequested = false;
|
|
486
640
|
const pendingData = [];
|
|
487
641
|
const pendingReads = [];
|
|
488
642
|
const channel = {
|
|
@@ -523,6 +677,7 @@ function createChannel(id, sendFn, closeFn) {
|
|
|
523
677
|
return this;
|
|
524
678
|
},
|
|
525
679
|
[Symbol.asyncIterator]() {
|
|
680
|
+
iteratorRequested = true;
|
|
526
681
|
return {
|
|
527
682
|
next() {
|
|
528
683
|
if (pendingData.length > 0) {
|
|
@@ -554,7 +709,7 @@ function createChannel(id, sendFn, closeFn) {
|
|
|
554
709
|
if (pendingReads.length > 0) {
|
|
555
710
|
pendingReads.shift().resolve({ done: false, value: payload });
|
|
556
711
|
}
|
|
557
|
-
else {
|
|
712
|
+
else if (iteratorRequested || listeners.data.size === 0) {
|
|
558
713
|
pendingData.push(payload);
|
|
559
714
|
}
|
|
560
715
|
for (const listener of listeners.data) {
|
|
@@ -642,7 +797,15 @@ function isTransportReady() {
|
|
|
642
797
|
communicationMethod === WEB_MESSAGE_TYPE);
|
|
643
798
|
}
|
|
644
799
|
function canSendAppMessages() {
|
|
645
|
-
|
|
800
|
+
if (!handshakeDone)
|
|
801
|
+
return false;
|
|
802
|
+
// Apple upstream posts through WKScriptMessageHandler independently of the
|
|
803
|
+
// downstream fetch. During a replay reconnect, native retains responses by
|
|
804
|
+
// sequence, so treating that short gap as an unsendable session can strand
|
|
805
|
+
// requests in the outbox with no second `ready` event to flush them.
|
|
806
|
+
if (useAppleDownstreamTransport())
|
|
807
|
+
return true;
|
|
808
|
+
return isTransportReady();
|
|
646
809
|
}
|
|
647
810
|
function rejectPendingRequest(reqId, err) {
|
|
648
811
|
const info = pendingReq.get(reqId);
|
|
@@ -1031,7 +1194,15 @@ function handleIncomingMessage(msg) {
|
|
|
1031
1194
|
if (pendingReq.has(message.id)) {
|
|
1032
1195
|
const req = pendingReq.get(message.id);
|
|
1033
1196
|
if (req?.mode === "stream" && req.stream) {
|
|
1034
|
-
|
|
1197
|
+
// The first frame proves the handler is live. Long-lived streams
|
|
1198
|
+
// (bookmarks.watch, proxy.watch) are idle between pushes, so the
|
|
1199
|
+
// request timeout must not treat silence as death — disarm it for
|
|
1200
|
+
// good instead of re-arming per frame.
|
|
1201
|
+
if (req.timerId !== null) {
|
|
1202
|
+
clearTimeout(req.timerId);
|
|
1203
|
+
req.timerId = null;
|
|
1204
|
+
}
|
|
1205
|
+
req.timeoutMs = 0;
|
|
1035
1206
|
req.stream._emitData(message.payload);
|
|
1036
1207
|
}
|
|
1037
1208
|
else {
|
|
@@ -1109,8 +1280,10 @@ function handleIncomingMessage(msg) {
|
|
|
1109
1280
|
case "ch.close": {
|
|
1110
1281
|
const pendingChannel = pendingChannels.get(message.id);
|
|
1111
1282
|
if (pendingChannel) {
|
|
1112
|
-
|
|
1113
|
-
|
|
1283
|
+
rejectPendingChannel(message.id, {
|
|
1284
|
+
code: message.code ?? BRIDGE_ERROR.STREAM_CLOSED,
|
|
1285
|
+
message: message.reason ?? "Channel closed before opening",
|
|
1286
|
+
});
|
|
1114
1287
|
return;
|
|
1115
1288
|
}
|
|
1116
1289
|
const channel = activeChannels.get(message.id);
|
|
@@ -1458,6 +1631,8 @@ export const LingXiaBridge = {
|
|
|
1458
1631
|
isMacOS,
|
|
1459
1632
|
isWindows,
|
|
1460
1633
|
isDesktop,
|
|
1634
|
+
isApple,
|
|
1635
|
+
isRunner,
|
|
1461
1636
|
getOS: getPlatformOS,
|
|
1462
1637
|
},
|
|
1463
1638
|
dom: {
|
|
@@ -1598,6 +1773,8 @@ export function initBridge() {
|
|
|
1598
1773
|
activateReceiver(LingXiaBridge._receiveEvaluateMessage);
|
|
1599
1774
|
if (useAppleDownstreamTransport()) {
|
|
1600
1775
|
ensureAppleDownstream();
|
|
1776
|
+
startAppleWatchdog();
|
|
1777
|
+
installAppleForegroundReconnect();
|
|
1601
1778
|
}
|
|
1602
1779
|
else if (communicationMethod === MESSAGE_PORT_TYPE) {
|
|
1603
1780
|
installMessagePortInitListener();
|
|
@@ -1612,6 +1789,9 @@ export function initBridge() {
|
|
|
1612
1789
|
warn("Unknown method");
|
|
1613
1790
|
}
|
|
1614
1791
|
window.LingXiaBridge = LingXiaBridge;
|
|
1792
|
+
// Dev hook so repro/test pages can simulate a WebKit stream replacement.
|
|
1793
|
+
window.__lxForceDownstreamReconnect =
|
|
1794
|
+
forceDownstreamReconnect;
|
|
1615
1795
|
installNativeComponentCoverageMonitor({
|
|
1616
1796
|
os: getPlatformOS(),
|
|
1617
1797
|
send: sendNativeComponentMessage,
|
package/dist/es2020/index.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { LingXiaBridge, channel, initBridge, invoke, notify, stream, } from './bridge';
|
|
9
9
|
export { isNativeError } from './invocation';
|
|
10
|
+
export { getDisplayLanguage } from './runtime-env';
|
|
10
11
|
export { renderErrorUI, hasError, getErrorInfo } from './error';
|
|
11
12
|
export { boot, bootWhenReady } from './boot';
|
|
12
13
|
import { bootWhenReady } from './boot';
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export const BRIDGE_CONFIG = (typeof window !== 'undefined' && window.__LX_BRIDGE_CFG) || {};
|
|
2
|
+
const displayLanguage = BRIDGE_CONFIG.displayLanguage?.trim() || 'en-US';
|
|
3
|
+
if (typeof document !== 'undefined' && document.documentElement) {
|
|
4
|
+
document.documentElement.lang = displayLanguage;
|
|
5
|
+
}
|
|
6
|
+
export function getDisplayLanguage() {
|
|
7
|
+
return displayLanguage;
|
|
8
|
+
}
|
|
2
9
|
export function getPlatformOS() {
|
|
3
10
|
return BRIDGE_CONFIG.os || 'unknown';
|
|
4
11
|
}
|
|
@@ -20,6 +27,24 @@ export function isWindows() {
|
|
|
20
27
|
export function isDesktop() {
|
|
21
28
|
return isMacOS() || isWindows();
|
|
22
29
|
}
|
|
30
|
+
// iOS and macOS share the WKWebView transport, so features scoped to it (e.g.
|
|
31
|
+
// the streaming downstream) key off this rather than the two OS checks.
|
|
32
|
+
export function isApple() {
|
|
33
|
+
return isIOS() || isMacOS();
|
|
34
|
+
}
|
|
35
|
+
// True when attached to a `lingxia dev` session (the host sets `dev` in
|
|
36
|
+
// `__LX_BRIDGE_CFG`). Used to surface the bridge's own protocol/lifecycle trace
|
|
37
|
+
// only during development.
|
|
38
|
+
export function isDevSession() {
|
|
39
|
+
return BRIDGE_CONFIG.dev === true;
|
|
40
|
+
}
|
|
41
|
+
// True when running inside the LingXia Runner (the `lingxia dev` device
|
|
42
|
+
// simulator), which the host marks in `__LX_BRIDGE_CFG`. Unlike a real host
|
|
43
|
+
// app in dev mode, the Runner lacks host-declared surfaces such as the
|
|
44
|
+
// terminal — apps read this to hide those affordances.
|
|
45
|
+
export function isRunner() {
|
|
46
|
+
return BRIDGE_CONFIG.runner === true;
|
|
47
|
+
}
|
|
23
48
|
export function getCommunicationMethod() {
|
|
24
49
|
if (BRIDGE_CONFIG.os === 'iOS' || BRIDGE_CONFIG.os === 'macOS')
|
|
25
50
|
return 'webkit';
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isDesktop } from '../runtime-env';
|
|
2
2
|
const ANIMATION_MS = 300;
|
|
3
3
|
const STYLE_ID = 'lx-action-sheet-style';
|
|
4
4
|
let activeSheet = null;
|
|
5
5
|
function isDesktopMode() {
|
|
6
|
-
return
|
|
6
|
+
return isDesktop();
|
|
7
7
|
}
|
|
8
8
|
function ensureActionSheetStyle() {
|
|
9
9
|
if (document.getElementById(STYLE_ID))
|