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