@lingxia/bridge 0.9.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 +225 -24
- package/dist/es2020/index.js +1 -0
- package/dist/es2020/runtime-env.js +31 -1
- package/dist/es2020/ui/action-sheet.js +2 -2
- package/dist/es5/bridge.js +274 -62
- package/dist/es5/index.js +1 -0
- package/dist/es5/runtime-env.js +32 -1
- package/dist/es5/ui/action-sheet.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/runtime-env.d.ts +6 -1
- package/dist/types.d.ts +8 -1
- package/package.json +2 -2
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, 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;
|
|
@@ -10,6 +10,7 @@ const HANDSHAKE_MAX_RETRIES = 3;
|
|
|
10
10
|
const LOG_PREFIX = "[LX.Bridge]";
|
|
11
11
|
const MESSAGE_PORT_TYPE = "messageport";
|
|
12
12
|
const JS_INTERFACE_TYPE = "jsinterface";
|
|
13
|
+
const WEB_MESSAGE_TYPE = "webmessage";
|
|
13
14
|
const OUTBOX_LIMIT = 256;
|
|
14
15
|
const APPLE_DOWNSTREAM_URL = BRIDGE_CONFIG.appleDownstreamURL;
|
|
15
16
|
const APPLE_RECONNECT_BASE_MS = 200;
|
|
@@ -64,7 +65,18 @@ function activateReceiver(receiver) {
|
|
|
64
65
|
function isDebugEnabled(flag) {
|
|
65
66
|
return debugFlags.all || debugFlags[flag];
|
|
66
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.
|
|
67
73
|
function log(...args) {
|
|
74
|
+
if (!isDevSession() &&
|
|
75
|
+
!debugFlags.all &&
|
|
76
|
+
!debugFlags.proto &&
|
|
77
|
+
!debugFlags.data) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
68
80
|
console.log(LOG_PREFIX, ...args);
|
|
69
81
|
}
|
|
70
82
|
function warn(...args) {
|
|
@@ -111,6 +123,17 @@ let appleDownstreamTask = null;
|
|
|
111
123
|
let appleDownstreamAbortController = null;
|
|
112
124
|
let appleReconnectTimer = null;
|
|
113
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;
|
|
114
137
|
const portInitState = {
|
|
115
138
|
listenerInstalled: false,
|
|
116
139
|
promise: null,
|
|
@@ -154,6 +177,23 @@ function clearAppleReconnectTimer() {
|
|
|
154
177
|
appleReconnectTimer = null;
|
|
155
178
|
}
|
|
156
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
|
+
}
|
|
157
197
|
function closeActiveChannelsFromTransport(reason) {
|
|
158
198
|
for (const [id, channel] of Array.from(activeChannels.entries())) {
|
|
159
199
|
activeChannels.delete(id);
|
|
@@ -195,7 +235,7 @@ function processAppleDownstreamBuffer(buffer) {
|
|
|
195
235
|
if (!line)
|
|
196
236
|
continue;
|
|
197
237
|
try {
|
|
198
|
-
|
|
238
|
+
handleAppleDownstreamFrame(JSON.parse(line));
|
|
199
239
|
}
|
|
200
240
|
catch (e) {
|
|
201
241
|
warn("Apple downstream parse error:", e, line);
|
|
@@ -203,13 +243,49 @@ function processAppleDownstreamBuffer(buffer) {
|
|
|
203
243
|
}
|
|
204
244
|
return remaining;
|
|
205
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
|
+
}
|
|
206
279
|
async function runAppleDownstream() {
|
|
207
280
|
if (!APPLE_DOWNSTREAM_URL) {
|
|
208
281
|
throw new Error("Apple downstream URL is not configured");
|
|
209
282
|
}
|
|
210
283
|
const controller = new AbortController();
|
|
211
284
|
appleDownstreamAbortController = controller;
|
|
212
|
-
|
|
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, {
|
|
213
289
|
method: "GET",
|
|
214
290
|
cache: "no-store",
|
|
215
291
|
headers: { Accept: "application/x-ndjson" },
|
|
@@ -218,11 +294,16 @@ async function runAppleDownstream() {
|
|
|
218
294
|
if (!response.ok) {
|
|
219
295
|
throw new Error(`Apple downstream HTTP ${response.status}`);
|
|
220
296
|
}
|
|
297
|
+
if (response.headers.get("X-LingXia-Bridge-Shutdown") === "1") {
|
|
298
|
+
disposeAppleDownstream("Apple downstream closed");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
221
301
|
if (!response.body) {
|
|
222
302
|
throw new Error("Apple downstream response body unavailable");
|
|
223
303
|
}
|
|
224
304
|
appleDownstreamConnected = true;
|
|
225
305
|
appleReconnectDelayMs = APPLE_RECONNECT_BASE_MS;
|
|
306
|
+
appleLastFrameAt = Date.now();
|
|
226
307
|
if (isDebugEnabled("proto"))
|
|
227
308
|
log("Apple downstream connected");
|
|
228
309
|
startHandshake();
|
|
@@ -241,7 +322,7 @@ async function runAppleDownstream() {
|
|
|
241
322
|
const tail = buffered.trim();
|
|
242
323
|
if (tail) {
|
|
243
324
|
try {
|
|
244
|
-
|
|
325
|
+
handleAppleDownstreamFrame(JSON.parse(tail));
|
|
245
326
|
}
|
|
246
327
|
catch (e) {
|
|
247
328
|
warn("Apple downstream trailing parse error:", e, tail);
|
|
@@ -260,13 +341,17 @@ async function runAppleDownstream() {
|
|
|
260
341
|
catch { }
|
|
261
342
|
}
|
|
262
343
|
}
|
|
263
|
-
function scheduleAppleDownstreamReconnect(reason) {
|
|
264
|
-
if (!useAppleDownstreamTransport())
|
|
344
|
+
function scheduleAppleDownstreamReconnect(reason, immediate = false) {
|
|
345
|
+
if (!useAppleDownstreamTransport() || appleDownstreamDisposed)
|
|
265
346
|
return;
|
|
266
347
|
clearAppleReconnectTimer();
|
|
267
|
-
const delay = appleReconnectDelayMs;
|
|
268
|
-
|
|
269
|
-
|
|
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}`;
|
|
270
355
|
if (delay <= APPLE_RECONNECT_BASE_MS) {
|
|
271
356
|
log(message);
|
|
272
357
|
}
|
|
@@ -279,12 +364,16 @@ function scheduleAppleDownstreamReconnect(reason) {
|
|
|
279
364
|
}, delay);
|
|
280
365
|
}
|
|
281
366
|
function ensureAppleDownstream() {
|
|
282
|
-
if (!useAppleDownstreamTransport())
|
|
367
|
+
if (!useAppleDownstreamTransport() || appleDownstreamDisposed)
|
|
283
368
|
return;
|
|
284
369
|
if (appleDownstreamTask)
|
|
285
370
|
return;
|
|
286
371
|
clearAppleReconnectTimer();
|
|
372
|
+
let completedCleanly = false;
|
|
287
373
|
appleDownstreamTask = runAppleDownstream()
|
|
374
|
+
.then(() => {
|
|
375
|
+
completedCleanly = true;
|
|
376
|
+
})
|
|
288
377
|
.catch((e) => {
|
|
289
378
|
if (appleDownstreamAbortController?.signal.aborted)
|
|
290
379
|
return;
|
|
@@ -294,13 +383,72 @@ function ensureAppleDownstream() {
|
|
|
294
383
|
const aborted = appleDownstreamAbortController?.signal.aborted ?? false;
|
|
295
384
|
appleDownstreamTask = null;
|
|
296
385
|
appleDownstreamAbortController = null;
|
|
297
|
-
const wasConnected = appleDownstreamConnected;
|
|
298
386
|
appleDownstreamConnected = false;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
+
}
|
|
302
397
|
});
|
|
303
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
|
+
}
|
|
304
452
|
function getMessagePort() {
|
|
305
453
|
if (messagePort)
|
|
306
454
|
return Promise.resolve(messagePort);
|
|
@@ -345,7 +493,8 @@ function postToNative(message) {
|
|
|
345
493
|
messagePort.postMessage(messageString);
|
|
346
494
|
return;
|
|
347
495
|
}
|
|
348
|
-
if (communicationMethod === JS_INTERFACE_TYPE
|
|
496
|
+
if ((communicationMethod === JS_INTERFACE_TYPE ||
|
|
497
|
+
communicationMethod === WEB_MESSAGE_TYPE) &&
|
|
349
498
|
window.LingXiaProxy?.postMessage) {
|
|
350
499
|
window.LingXiaProxy.postMessage(messageString);
|
|
351
500
|
return;
|
|
@@ -378,6 +527,9 @@ function createListenerBuckets() {
|
|
|
378
527
|
function createStreamHandle(id, cancelFn) {
|
|
379
528
|
const listeners = createListenerBuckets();
|
|
380
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;
|
|
381
533
|
let resolveResult = () => { };
|
|
382
534
|
let rejectResult = () => { };
|
|
383
535
|
const pendingData = [];
|
|
@@ -399,6 +551,7 @@ function createStreamHandle(id, cancelFn) {
|
|
|
399
551
|
return this;
|
|
400
552
|
},
|
|
401
553
|
[Symbol.asyncIterator]() {
|
|
554
|
+
iteratorRequested = true;
|
|
402
555
|
return {
|
|
403
556
|
next() {
|
|
404
557
|
if (pendingData.length > 0) {
|
|
@@ -427,7 +580,7 @@ function createStreamHandle(id, cancelFn) {
|
|
|
427
580
|
if (pendingReads.length > 0) {
|
|
428
581
|
pendingReads.shift().resolve({ done: false, value: payload });
|
|
429
582
|
}
|
|
430
|
-
else {
|
|
583
|
+
else if (iteratorRequested) {
|
|
431
584
|
pendingData.push(payload);
|
|
432
585
|
}
|
|
433
586
|
for (const listener of listeners.data) {
|
|
@@ -481,6 +634,9 @@ function createChannel(id, sendFn, closeFn) {
|
|
|
481
634
|
let open = false;
|
|
482
635
|
let outboundSeq = 0;
|
|
483
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;
|
|
484
640
|
const pendingData = [];
|
|
485
641
|
const pendingReads = [];
|
|
486
642
|
const channel = {
|
|
@@ -496,8 +652,24 @@ function createChannel(id, sendFn, closeFn) {
|
|
|
496
652
|
sendFn(payload, outboundSeq++);
|
|
497
653
|
},
|
|
498
654
|
on(event, listener) {
|
|
499
|
-
if (event === "data")
|
|
655
|
+
if (event === "data") {
|
|
500
656
|
listeners.data.add(listener);
|
|
657
|
+
// Flush data buffered before this listener attached. The host can send
|
|
658
|
+
// ch.data immediately after the open ack, before the consumer (e.g. a
|
|
659
|
+
// wrapped channel's onMessage) attaches its listener; without this those
|
|
660
|
+
// early events would sit in pendingData and never reach the listener.
|
|
661
|
+
if (pendingData.length > 0) {
|
|
662
|
+
const buffered = pendingData.splice(0);
|
|
663
|
+
for (const payload of buffered) {
|
|
664
|
+
try {
|
|
665
|
+
listener(payload);
|
|
666
|
+
}
|
|
667
|
+
catch (e) {
|
|
668
|
+
warn("Channel listener failed:", e);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
501
673
|
if (event === "close")
|
|
502
674
|
listeners.close.add(listener);
|
|
503
675
|
if (event === "error")
|
|
@@ -505,6 +677,7 @@ function createChannel(id, sendFn, closeFn) {
|
|
|
505
677
|
return this;
|
|
506
678
|
},
|
|
507
679
|
[Symbol.asyncIterator]() {
|
|
680
|
+
iteratorRequested = true;
|
|
508
681
|
return {
|
|
509
682
|
next() {
|
|
510
683
|
if (pendingData.length > 0) {
|
|
@@ -536,7 +709,7 @@ function createChannel(id, sendFn, closeFn) {
|
|
|
536
709
|
if (pendingReads.length > 0) {
|
|
537
710
|
pendingReads.shift().resolve({ done: false, value: payload });
|
|
538
711
|
}
|
|
539
|
-
else {
|
|
712
|
+
else if (iteratorRequested || listeners.data.size === 0) {
|
|
540
713
|
pendingData.push(payload);
|
|
541
714
|
}
|
|
542
715
|
for (const listener of listeners.data) {
|
|
@@ -620,10 +793,19 @@ function isTransportReady() {
|
|
|
620
793
|
if (useAppleDownstreamTransport())
|
|
621
794
|
return appleDownstreamConnected;
|
|
622
795
|
return (communicationMethod === "webkit" ||
|
|
623
|
-
communicationMethod === JS_INTERFACE_TYPE
|
|
796
|
+
communicationMethod === JS_INTERFACE_TYPE ||
|
|
797
|
+
communicationMethod === WEB_MESSAGE_TYPE);
|
|
624
798
|
}
|
|
625
799
|
function canSendAppMessages() {
|
|
626
|
-
|
|
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();
|
|
627
809
|
}
|
|
628
810
|
function rejectPendingRequest(reqId, err) {
|
|
629
811
|
const info = pendingReq.get(reqId);
|
|
@@ -1012,7 +1194,15 @@ function handleIncomingMessage(msg) {
|
|
|
1012
1194
|
if (pendingReq.has(message.id)) {
|
|
1013
1195
|
const req = pendingReq.get(message.id);
|
|
1014
1196
|
if (req?.mode === "stream" && req.stream) {
|
|
1015
|
-
|
|
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;
|
|
1016
1206
|
req.stream._emitData(message.payload);
|
|
1017
1207
|
}
|
|
1018
1208
|
else {
|
|
@@ -1090,8 +1280,10 @@ function handleIncomingMessage(msg) {
|
|
|
1090
1280
|
case "ch.close": {
|
|
1091
1281
|
const pendingChannel = pendingChannels.get(message.id);
|
|
1092
1282
|
if (pendingChannel) {
|
|
1093
|
-
|
|
1094
|
-
|
|
1283
|
+
rejectPendingChannel(message.id, {
|
|
1284
|
+
code: message.code ?? BRIDGE_ERROR.STREAM_CLOSED,
|
|
1285
|
+
message: message.reason ?? "Channel closed before opening",
|
|
1286
|
+
});
|
|
1095
1287
|
return;
|
|
1096
1288
|
}
|
|
1097
1289
|
const channel = activeChannels.get(message.id);
|
|
@@ -1437,7 +1629,10 @@ export const LingXiaBridge = {
|
|
|
1437
1629
|
isIOS,
|
|
1438
1630
|
isAndroid,
|
|
1439
1631
|
isMacOS,
|
|
1632
|
+
isWindows,
|
|
1440
1633
|
isDesktop,
|
|
1634
|
+
isApple,
|
|
1635
|
+
isRunner,
|
|
1441
1636
|
getOS: getPlatformOS,
|
|
1442
1637
|
},
|
|
1443
1638
|
dom: {
|
|
@@ -1578,19 +1773,25 @@ export function initBridge() {
|
|
|
1578
1773
|
activateReceiver(LingXiaBridge._receiveEvaluateMessage);
|
|
1579
1774
|
if (useAppleDownstreamTransport()) {
|
|
1580
1775
|
ensureAppleDownstream();
|
|
1776
|
+
startAppleWatchdog();
|
|
1777
|
+
installAppleForegroundReconnect();
|
|
1581
1778
|
}
|
|
1582
1779
|
else if (communicationMethod === MESSAGE_PORT_TYPE) {
|
|
1583
1780
|
installMessagePortInitListener();
|
|
1584
1781
|
getMessagePort().catch((e) => warn("Port init failed:", e));
|
|
1585
1782
|
}
|
|
1586
1783
|
else if (communicationMethod === "webkit" ||
|
|
1587
|
-
communicationMethod === JS_INTERFACE_TYPE
|
|
1784
|
+
communicationMethod === JS_INTERFACE_TYPE ||
|
|
1785
|
+
communicationMethod === WEB_MESSAGE_TYPE) {
|
|
1588
1786
|
startHandshake();
|
|
1589
1787
|
}
|
|
1590
1788
|
else {
|
|
1591
1789
|
warn("Unknown method");
|
|
1592
1790
|
}
|
|
1593
1791
|
window.LingXiaBridge = LingXiaBridge;
|
|
1792
|
+
// Dev hook so repro/test pages can simulate a WebKit stream replacement.
|
|
1793
|
+
window.__lxForceDownstreamReconnect =
|
|
1794
|
+
forceDownstreamReconnect;
|
|
1594
1795
|
installNativeComponentCoverageMonitor({
|
|
1595
1796
|
os: getPlatformOS(),
|
|
1596
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
|
}
|
|
@@ -14,14 +21,37 @@ export function isAndroid() {
|
|
|
14
21
|
export function isMacOS() {
|
|
15
22
|
return BRIDGE_CONFIG.os === 'macOS';
|
|
16
23
|
}
|
|
24
|
+
export function isWindows() {
|
|
25
|
+
return BRIDGE_CONFIG.os === 'Windows';
|
|
26
|
+
}
|
|
17
27
|
export function isDesktop() {
|
|
18
|
-
return isMacOS();
|
|
28
|
+
return isMacOS() || isWindows();
|
|
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;
|
|
19
47
|
}
|
|
20
48
|
export function getCommunicationMethod() {
|
|
21
49
|
if (BRIDGE_CONFIG.os === 'iOS' || BRIDGE_CONFIG.os === 'macOS')
|
|
22
50
|
return 'webkit';
|
|
23
51
|
if (BRIDGE_CONFIG.os === 'Harmony')
|
|
24
52
|
return 'messageport';
|
|
53
|
+
if (BRIDGE_CONFIG.os === 'Windows')
|
|
54
|
+
return 'webmessage';
|
|
25
55
|
if (BRIDGE_CONFIG.os === 'Android') {
|
|
26
56
|
if (window.LingXiaProxy?.supportsMessagePort?.())
|
|
27
57
|
return 'messageport';
|
|
@@ -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))
|