@lingxia/bridge 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,8 +58,7 @@ function escapeHtml(str) {
58
58
  div.textContent = str;
59
59
  return div.innerHTML;
60
60
  }
61
- export function renderErrorUI(errorInfo) {
62
- const { failedPath, reason } = errorInfo;
61
+ function renderPanel({ code, title, description, detail, reason }) {
63
62
  const styleEl = document.createElement('style');
64
63
  styleEl.textContent = ERROR_STYLES;
65
64
  document.head.appendChild(styleEl);
@@ -67,11 +66,11 @@ export function renderErrorUI(errorInfo) {
67
66
  document.body.style.cssText = '';
68
67
  const container = document.createElement('div');
69
68
  container.className = 'lx-error';
70
- let html = '<div class="lx-error-code">404</div>';
71
- html += '<h1 class="lx-error-title">Page Not Found</h1>';
72
- html += '<p class="lx-error-desc">The page you\'re looking for doesn\'t exist.</p>';
73
- if (failedPath) {
74
- html += `<div class="lx-error-path">${escapeHtml(failedPath)}</div>`;
69
+ let html = `<div class="lx-error-code">${escapeHtml(code)}</div>`;
70
+ html += `<h1 class="lx-error-title">${escapeHtml(title)}</h1>`;
71
+ html += `<p class="lx-error-desc">${escapeHtml(description)}</p>`;
72
+ if (detail) {
73
+ html += `<div class="lx-error-path">${escapeHtml(detail)}</div>`;
75
74
  }
76
75
  if (reason) {
77
76
  html += `<p class="lx-error-desc" style="margin-top:12px">${escapeHtml(reason)}</p>`;
@@ -79,6 +78,48 @@ export function renderErrorUI(errorInfo) {
79
78
  container.innerHTML = html;
80
79
  document.body.appendChild(container);
81
80
  }
81
+ export function renderErrorUI(errorInfo) {
82
+ renderPanel({
83
+ code: '404',
84
+ title: 'Page Not Found',
85
+ description: "The page you're looking for doesn't exist.",
86
+ detail: errorInfo.failedPath,
87
+ reason: errorInfo.reason,
88
+ });
89
+ }
90
+ /**
91
+ * A page whose Logic has not delivered its first state in time — it failed to
92
+ * load, threw before the page could start, or is very slow. Shown over the page
93
+ * instead of a blank screen, naming the page; returns a function that removes
94
+ * it, for when the state does arrive after all.
95
+ */
96
+ export function renderPageFault(pagePath, reason) {
97
+ const styleEl = document.createElement('style');
98
+ styleEl.textContent = `
99
+ .lx-page-fault { position: fixed; inset: 0; z-index: 2147483647; overflow: auto;
100
+ background: #fff; color: #1d1d1f; }
101
+ @media (prefers-color-scheme: dark) { .lx-page-fault { background: #000; color: #f5f5f7; } }
102
+ ${ERROR_STYLES.replace(/\bbody\b/g, '.lx-page-fault')}`;
103
+ document.head.appendChild(styleEl);
104
+ const overlay = document.createElement('div');
105
+ overlay.className = 'lx-page-fault';
106
+ overlay.setAttribute('role', 'alert');
107
+ const container = document.createElement('div');
108
+ container.className = 'lx-error';
109
+ let html = '<div class="lx-error-code">Page</div>';
110
+ html += `<h1 class="lx-error-title">${escapeHtml("This page couldn't start")}</h1>`;
111
+ html += `<p class="lx-error-desc">${escapeHtml('Its Logic has not delivered the page state. Check the Logic log for an error.')}</p>`;
112
+ if (pagePath)
113
+ html += `<div class="lx-error-path">${escapeHtml(pagePath)}</div>`;
114
+ html += `<p class="lx-error-desc" style="margin-top:12px">${escapeHtml(reason)}</p>`;
115
+ container.innerHTML = html;
116
+ overlay.appendChild(container);
117
+ document.body.appendChild(overlay);
118
+ return () => {
119
+ overlay.remove();
120
+ styleEl.remove();
121
+ };
122
+ }
82
123
  export function hasError() {
83
124
  const config = window.__LX_RUNTIME_CONFIG;
84
125
  return !!(config && config.error);
@@ -0,0 +1,53 @@
1
+ import { BRIDGE_CONFIG, getDisplayLanguage, getPlatformOS, isDesktop, subscribeDisplayLanguage, } from './runtime-env';
2
+ import { getSurfaceContext, subscribeSurfaceContext } from './surface-context';
3
+ let current = null;
4
+ function read() {
5
+ const surface = getSurfaceContext();
6
+ return {
7
+ sizeClass: surface.sizeClass,
8
+ aside: surface.aside,
9
+ displayLanguage: getDisplayLanguage(),
10
+ formFactor: isDesktop() ? 'desktop' : 'mobile',
11
+ os: getPlatformOS(),
12
+ runner: BRIDGE_CONFIG.runner === true,
13
+ };
14
+ }
15
+ function same(a, b) {
16
+ return a.sizeClass === b.sizeClass
17
+ && a.aside === b.aside
18
+ && a.displayLanguage === b.displayLanguage
19
+ && a.formFactor === b.formFactor
20
+ && a.os === b.os
21
+ && a.runner === b.runner;
22
+ }
23
+ /**
24
+ * The host facts for this page. The same object is returned until a field
25
+ * changes, so it composes with `useSyncExternalStore` and with memoization.
26
+ */
27
+ export function getHost() {
28
+ const next = read();
29
+ if (current === null || !same(current, next))
30
+ current = next;
31
+ return current;
32
+ }
33
+ /**
34
+ * Follow the host facts. Change-only: the listener runs when a field of
35
+ * `getHost()` changes — not on subscribe, and not when only the viewport's
36
+ * size moves within its size class. Returns an unsubscribe.
37
+ */
38
+ export function subscribeHost(listener) {
39
+ let last = getHost();
40
+ const check = () => {
41
+ const next = getHost();
42
+ if (next === last)
43
+ return;
44
+ last = next;
45
+ listener();
46
+ };
47
+ const stopLanguage = subscribeDisplayLanguage(check);
48
+ const stopSurface = subscribeSurfaceContext(check);
49
+ return () => {
50
+ stopLanguage();
51
+ stopSurface();
52
+ };
53
+ }
@@ -7,8 +7,8 @@
7
7
  */
8
8
  export { LingXiaBridge, channel, initBridge, invoke, notify, stream, } from './bridge';
9
9
  export { isNativeError } from './invocation';
10
- export { getDisplayLanguage, subscribeDisplayLanguage } from './runtime-env';
11
- export { renderErrorUI, hasError, getErrorInfo } from './error';
10
+ export { getHost, subscribeHost } from './host';
11
+ export { renderErrorUI, renderPageFault, hasError, getErrorInfo } from './error';
12
12
  export { boot, bootWhenReady } from './boot';
13
13
  import { bootWhenReady } from './boot';
14
14
  import { registerUIHandlers } from './ui';
@@ -23,9 +23,30 @@ function store() {
23
23
  }
24
24
  return window.__lxDisplayLanguage;
25
25
  }
26
+ /** Primary subtags of languages written right to left, for engines without `Intl.Locale#getTextInfo`. */
27
+ const RTL_LANGUAGES = new Set(['ar', 'ckb', 'dv', 'fa', 'he', 'ps', 'sd', 'ug', 'ur', 'yi']);
28
+ /** The writing direction of a BCP-47 tag. */
29
+ export function textDirection(tag) {
30
+ try {
31
+ const locale = new Intl.Locale(tag);
32
+ const info = typeof locale.getTextInfo === 'function' ? locale.getTextInfo() : locale.textInfo;
33
+ if (info?.direction === 'rtl' || info?.direction === 'ltr')
34
+ return info.direction;
35
+ }
36
+ catch {
37
+ // Not a tag `Intl` accepts; fall through to the subtag list.
38
+ }
39
+ return RTL_LANGUAGES.has(tag.split(/[-_]/)[0].toLowerCase()) ? 'rtl' : 'ltr';
40
+ }
41
+ /**
42
+ * `lang` and `dir` on `<html>` follow the display language, so a page never
43
+ * sets either: CSS logical properties and `:dir()` just work.
44
+ */
26
45
  function stampDocumentLanguage() {
27
46
  if (typeof document !== 'undefined' && document.documentElement) {
28
- document.documentElement.lang = store().value;
47
+ const language = store().value;
48
+ document.documentElement.lang = language;
49
+ document.documentElement.dir = textDirection(language);
29
50
  }
30
51
  }
31
52
  stampDocumentLanguage();
@@ -33,10 +54,21 @@ stampDocumentLanguage();
33
54
  * Host entry point for a language the user changed while this document was
34
55
  * open. Bootstrap alone would leave a live page in the language it started in,
35
56
  * with the native chrome around it already switched.
57
+ *
58
+ * The host pushes each change once, and again — the current value — when a
59
+ * document's bridge reports ready, since a change made while it loaded found
60
+ * no hook to call. Those two can land in either order, so a push carries the
61
+ * host revision it took effect at and an older one never overwrites a newer
62
+ * one. A push without a revision applies as it always did.
36
63
  */
37
- function applyDisplayLanguage(next) {
64
+ function applyDisplayLanguage(next, revision) {
38
65
  const normalized = typeof next === 'string' ? next.trim() : '';
39
66
  const current = store();
67
+ if (typeof revision === 'number' && Number.isFinite(revision)) {
68
+ if (revision <= (current.revision ?? 0))
69
+ return;
70
+ current.revision = revision;
71
+ }
40
72
  if (!normalized || normalized === current.value)
41
73
  return;
42
74
  current.value = normalized;
@@ -0,0 +1,86 @@
1
+ /** What a host that sends nothing — one older than this API — is taken to say. */
2
+ const UNREPORTED = { sizeClass: 'compact', width: 0, height: 0, aside: false };
3
+ /**
4
+ * One store per document, on `window`, for the same reason as the display
5
+ * language: the bridge module is present twice in a page, and the host must
6
+ * reach the copy the framework hooks read.
7
+ */
8
+ function store() {
9
+ if (typeof window === 'undefined')
10
+ return fallbackStore;
11
+ if (!window.__lxSurfaceContext) {
12
+ // Seeded by the host in this document's bridge config, before any script.
13
+ const config = window.__LX_BRIDGE_CFG;
14
+ window.__lxSurfaceContext = {
15
+ value: parse(config?.surfaceContext) ?? UNREPORTED,
16
+ revision: typeof config?.surfaceContextRevision === 'number' ? config.surfaceContextRevision : 0,
17
+ listeners: new Set(),
18
+ };
19
+ }
20
+ return window.__lxSurfaceContext;
21
+ }
22
+ const fallbackStore = { value: UNREPORTED, revision: 0, listeners: new Set() };
23
+ function parse(next) {
24
+ if (typeof next !== 'object' || next === null)
25
+ return null;
26
+ const { sizeClass, width, height, aside } = next;
27
+ if (sizeClass !== 'compact' && sizeClass !== 'regular')
28
+ return null;
29
+ if (typeof width !== 'number' || typeof height !== 'number')
30
+ return null;
31
+ return { sizeClass, width, height, aside: aside === true };
32
+ }
33
+ function same(a, b) {
34
+ return a.sizeClass === b.sizeClass
35
+ && a.width === b.width
36
+ && a.height === b.height
37
+ && a.aside === b.aside;
38
+ }
39
+ /**
40
+ * Host entry point: once the page's bridge is up, and on every change. Pushes
41
+ * can run out of order across host threads, so each carries a revision and an
42
+ * older one than the store holds is dropped.
43
+ */
44
+ function applySurfaceContext(next, revision) {
45
+ const context = parse(next);
46
+ const current = store();
47
+ if (!context)
48
+ return;
49
+ if (typeof revision === 'number') {
50
+ if (revision <= current.revision)
51
+ return;
52
+ current.revision = revision;
53
+ }
54
+ if (same(current.value, context))
55
+ return;
56
+ // A new object per change, so `useSyncExternalStore` sees it changed.
57
+ current.value = context;
58
+ for (const listener of [...current.listeners])
59
+ listener();
60
+ }
61
+ if (typeof window !== 'undefined' && !window.__lingxiaApplySurfaceContext) {
62
+ Object.defineProperty(window, '__lingxiaApplySurfaceContext', {
63
+ configurable: false,
64
+ enumerable: false,
65
+ value: applySurfaceContext,
66
+ });
67
+ }
68
+ /**
69
+ * The lxapp's adaptive context. The host seeds it into every page before any
70
+ * script runs and pushes each change, so there is always a value; a host older
71
+ * than this API reports `compact` with a 0×0 viewport.
72
+ */
73
+ export function getSurfaceContext() {
74
+ return store().value;
75
+ }
76
+ /**
77
+ * Follow the adaptive context. Change-only, like `subscribeDisplayLanguage`,
78
+ * so it composes with `useSyncExternalStore`. Returns an unsubscribe.
79
+ */
80
+ export function subscribeSurfaceContext(listener) {
81
+ const listeners = store().listeners;
82
+ listeners.add(listener);
83
+ return () => {
84
+ listeners.delete(listener);
85
+ };
86
+ }
@@ -96,7 +96,8 @@ import { BRIDGE_ERROR } from "./types";
96
96
  import { toBridgeError, toNativeError } from "./invocation";
97
97
  import { installNativeComponentCoverageMonitor } from "./nativecomponents/coverage-monitor";
98
98
  import { consumeV3Bootstrap, } from "./protocol-v3";
99
- import { BRIDGE_CONFIG, getCommunicationMethod, getDisplayLanguage, getPlatformOS, subscribeDisplayLanguage, isAndroid, isHarmony, isIOS, isMacOS, isWindows, isDesktop, isMobile, isApple, isDevSession, isRunner, } from "./runtime-env";
99
+ import { getHost, subscribeHost } from "./host";
100
+ import { BRIDGE_CONFIG, getCommunicationMethod, getPlatformOS, isAndroid, isHarmony, isIOS, isMacOS, isWindows, isDesktop, isMobile, isApple, isDevSession, isRunner, } from "./runtime-env";
100
101
  var NATIVE_HANDLER_NAME = "LingXia";
101
102
  var GLOBAL_RECEIVER_NAME = "__LingXiaRecvMessage";
102
103
  var DEFAULT_TIMEOUT_MS = 5000;
@@ -930,6 +931,16 @@ function createStreamHandle(id, cancelFn) {
930
931
  pendingReads.push({ resolve: resolve, reject: reject });
931
932
  });
932
933
  },
934
+ // Leaving a `for await` early cancels the stream, as with a
935
+ // ReadableStream: nothing is left buffering for a gone reader.
936
+ return: function () {
937
+ iteratorRequested = false;
938
+ pendingData.length = 0;
939
+ // The cancel settles `result` with BRIDGE_CANCELED; the reader asked for it.
940
+ result.catch(function () { });
941
+ handle.cancel();
942
+ return Promise.resolve({ done: true, value: undefined });
943
+ },
933
944
  };
934
945
  },
935
946
  _a.cancel = function () {
@@ -1031,6 +1042,7 @@ function createChannel(id, sendFn, closeFn) {
1031
1042
  var open = false;
1032
1043
  var outboundSeq = 0;
1033
1044
  var closed = false;
1045
+ var closeEmitted = false;
1034
1046
  // Only buffer for the async iterator or pre-first-listener replay;
1035
1047
  // otherwise a listener-driven channel would accumulate payloads forever.
1036
1048
  var iteratorRequested = false;
@@ -1102,13 +1114,20 @@ function createChannel(id, sendFn, closeFn) {
1102
1114
  pendingReads.push({ resolve: resolve, reject: reject });
1103
1115
  });
1104
1116
  },
1117
+ // Leaving a `for await` early closes the channel, as with a
1118
+ // ReadableStream: nothing is left buffering for a gone reader.
1119
+ return: function () {
1120
+ iteratorRequested = false;
1121
+ pendingData.length = 0;
1122
+ channel.close();
1123
+ return Promise.resolve({ done: true, value: undefined });
1124
+ },
1105
1125
  };
1106
1126
  },
1107
1127
  _a.close = function (code, reason) {
1108
1128
  if (!open)
1109
1129
  return;
1110
1130
  open = false;
1111
- closed = true;
1112
1131
  closeFn(code, reason);
1113
1132
  channel._emitClose(code, reason);
1114
1133
  },
@@ -1143,7 +1162,12 @@ function createChannel(id, sendFn, closeFn) {
1143
1162
  },
1144
1163
  _a._emitClose = function (code, reason) {
1145
1164
  var e_14, _a;
1165
+ // One close notification, whichever side closed first.
1166
+ open = false;
1146
1167
  closed = true;
1168
+ if (closeEmitted)
1169
+ return;
1170
+ closeEmitted = true;
1147
1171
  while (pendingReads.length > 0) {
1148
1172
  pendingReads.shift().resolve({ done: true, value: undefined });
1149
1173
  }
@@ -1168,6 +1192,7 @@ function createChannel(id, sendFn, closeFn) {
1168
1192
  },
1169
1193
  _a._reject = function (err) {
1170
1194
  var e_15, _a;
1195
+ open = false;
1171
1196
  closed = true;
1172
1197
  while (pendingReads.length > 0) {
1173
1198
  pendingReads.shift().reject(err);
@@ -2181,11 +2206,11 @@ export var LingXiaBridge = {
2181
2206
  handleIncomingMessage(messageString);
2182
2207
  },
2183
2208
  debug: createDebugObject(debugFlags),
2184
- // The host language, for documents that cannot bundle `@lingxia/bridge` —
2209
+ // The host facts, for documents that cannot bundle `@lingxia/bridge` —
2185
2210
  // browser internal pages and plain-HTML lxapps load this runtime as a script.
2186
- displayLanguage: {
2187
- get: getDisplayLanguage,
2188
- subscribe: subscribeDisplayLanguage,
2211
+ host: {
2212
+ get: getHost,
2213
+ subscribe: subscribeHost,
2189
2214
  },
2190
2215
  platform: {
2191
2216
  isHarmony: isHarmony,
@@ -2308,8 +2333,19 @@ function wrapNativeStream(handle) {
2308
2333
  function wrapNativeChannel(channel) {
2309
2334
  var messageListeners = new Set();
2310
2335
  var closeListeners = new Set();
2336
+ // Messages that arrive with the open ack, before the caller could call
2337
+ // `onMessage`, wait for it until the next task; a channel nobody listens
2338
+ // to buffers nothing after that.
2339
+ var early = [];
2340
+ setTimeout(function () {
2341
+ early = null;
2342
+ }, 0);
2311
2343
  channel.on("data", function (message) {
2312
2344
  var e_21, _a;
2345
+ if (messageListeners.size === 0) {
2346
+ early === null || early === void 0 ? void 0 : early.push(message);
2347
+ return;
2348
+ }
2313
2349
  try {
2314
2350
  for (var messageListeners_1 = __values(messageListeners), messageListeners_1_1 = messageListeners_1.next(); !messageListeners_1_1.done; messageListeners_1_1 = messageListeners_1.next()) {
2315
2351
  var listener = messageListeners_1_1.value;
@@ -2346,7 +2382,23 @@ function wrapNativeChannel(channel) {
2346
2382
  channel.send(message);
2347
2383
  },
2348
2384
  onMessage: function (listener) {
2385
+ var e_23, _a;
2349
2386
  messageListeners.add(listener);
2387
+ var replay = early;
2388
+ early = null;
2389
+ try {
2390
+ for (var _b = __values(replay !== null && replay !== void 0 ? replay : []), _c = _b.next(); !_c.done; _c = _b.next()) {
2391
+ var message = _c.value;
2392
+ listener(message);
2393
+ }
2394
+ }
2395
+ catch (e_23_1) { e_23 = { error: e_23_1 }; }
2396
+ finally {
2397
+ try {
2398
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
2399
+ }
2400
+ finally { if (e_23) throw e_23.error; }
2401
+ }
2350
2402
  return function () {
2351
2403
  messageListeners.delete(listener);
2352
2404
  };
package/dist/es5/error.js CHANGED
@@ -4,8 +4,8 @@ function escapeHtml(str) {
4
4
  div.textContent = str;
5
5
  return div.innerHTML;
6
6
  }
7
- export function renderErrorUI(errorInfo) {
8
- var failedPath = errorInfo.failedPath, reason = errorInfo.reason;
7
+ function renderPanel(_a) {
8
+ var code = _a.code, title = _a.title, description = _a.description, detail = _a.detail, reason = _a.reason;
9
9
  var styleEl = document.createElement('style');
10
10
  styleEl.textContent = ERROR_STYLES;
11
11
  document.head.appendChild(styleEl);
@@ -13,11 +13,11 @@ export function renderErrorUI(errorInfo) {
13
13
  document.body.style.cssText = '';
14
14
  var container = document.createElement('div');
15
15
  container.className = 'lx-error';
16
- var html = '<div class="lx-error-code">404</div>';
17
- html += '<h1 class="lx-error-title">Page Not Found</h1>';
18
- html += '<p class="lx-error-desc">The page you\'re looking for doesn\'t exist.</p>';
19
- if (failedPath) {
20
- html += "<div class=\"lx-error-path\">".concat(escapeHtml(failedPath), "</div>");
16
+ var html = "<div class=\"lx-error-code\">".concat(escapeHtml(code), "</div>");
17
+ html += "<h1 class=\"lx-error-title\">".concat(escapeHtml(title), "</h1>");
18
+ html += "<p class=\"lx-error-desc\">".concat(escapeHtml(description), "</p>");
19
+ if (detail) {
20
+ html += "<div class=\"lx-error-path\">".concat(escapeHtml(detail), "</div>");
21
21
  }
22
22
  if (reason) {
23
23
  html += "<p class=\"lx-error-desc\" style=\"margin-top:12px\">".concat(escapeHtml(reason), "</p>");
@@ -25,6 +25,44 @@ export function renderErrorUI(errorInfo) {
25
25
  container.innerHTML = html;
26
26
  document.body.appendChild(container);
27
27
  }
28
+ export function renderErrorUI(errorInfo) {
29
+ renderPanel({
30
+ code: '404',
31
+ title: 'Page Not Found',
32
+ description: "The page you're looking for doesn't exist.",
33
+ detail: errorInfo.failedPath,
34
+ reason: errorInfo.reason,
35
+ });
36
+ }
37
+ /**
38
+ * A page whose Logic has not delivered its first state in time — it failed to
39
+ * load, threw before the page could start, or is very slow. Shown over the page
40
+ * instead of a blank screen, naming the page; returns a function that removes
41
+ * it, for when the state does arrive after all.
42
+ */
43
+ export function renderPageFault(pagePath, reason) {
44
+ var styleEl = document.createElement('style');
45
+ styleEl.textContent = "\n.lx-page-fault { position: fixed; inset: 0; z-index: 2147483647; overflow: auto;\n background: #fff; color: #1d1d1f; }\n@media (prefers-color-scheme: dark) { .lx-page-fault { background: #000; color: #f5f5f7; } }\n".concat(ERROR_STYLES.replace(/\bbody\b/g, '.lx-page-fault'));
46
+ document.head.appendChild(styleEl);
47
+ var overlay = document.createElement('div');
48
+ overlay.className = 'lx-page-fault';
49
+ overlay.setAttribute('role', 'alert');
50
+ var container = document.createElement('div');
51
+ container.className = 'lx-error';
52
+ var html = '<div class="lx-error-code">Page</div>';
53
+ html += "<h1 class=\"lx-error-title\">".concat(escapeHtml("This page couldn't start"), "</h1>");
54
+ html += "<p class=\"lx-error-desc\">".concat(escapeHtml('Its Logic has not delivered the page state. Check the Logic log for an error.'), "</p>");
55
+ if (pagePath)
56
+ html += "<div class=\"lx-error-path\">".concat(escapeHtml(pagePath), "</div>");
57
+ html += "<p class=\"lx-error-desc\" style=\"margin-top:12px\">".concat(escapeHtml(reason), "</p>");
58
+ container.innerHTML = html;
59
+ overlay.appendChild(container);
60
+ document.body.appendChild(overlay);
61
+ return function () {
62
+ overlay.remove();
63
+ styleEl.remove();
64
+ };
65
+ }
28
66
  export function hasError() {
29
67
  var config = window.__LX_RUNTIME_CONFIG;
30
68
  return !!(config && config.error);
@@ -0,0 +1,53 @@
1
+ import { BRIDGE_CONFIG, getDisplayLanguage, getPlatformOS, isDesktop, subscribeDisplayLanguage, } from './runtime-env';
2
+ import { getSurfaceContext, subscribeSurfaceContext } from './surface-context';
3
+ var current = null;
4
+ function read() {
5
+ var surface = getSurfaceContext();
6
+ return {
7
+ sizeClass: surface.sizeClass,
8
+ aside: surface.aside,
9
+ displayLanguage: getDisplayLanguage(),
10
+ formFactor: isDesktop() ? 'desktop' : 'mobile',
11
+ os: getPlatformOS(),
12
+ runner: BRIDGE_CONFIG.runner === true,
13
+ };
14
+ }
15
+ function same(a, b) {
16
+ return a.sizeClass === b.sizeClass
17
+ && a.aside === b.aside
18
+ && a.displayLanguage === b.displayLanguage
19
+ && a.formFactor === b.formFactor
20
+ && a.os === b.os
21
+ && a.runner === b.runner;
22
+ }
23
+ /**
24
+ * The host facts for this page. The same object is returned until a field
25
+ * changes, so it composes with `useSyncExternalStore` and with memoization.
26
+ */
27
+ export function getHost() {
28
+ var next = read();
29
+ if (current === null || !same(current, next))
30
+ current = next;
31
+ return current;
32
+ }
33
+ /**
34
+ * Follow the host facts. Change-only: the listener runs when a field of
35
+ * `getHost()` changes — not on subscribe, and not when only the viewport's
36
+ * size moves within its size class. Returns an unsubscribe.
37
+ */
38
+ export function subscribeHost(listener) {
39
+ var last = getHost();
40
+ var check = function () {
41
+ var next = getHost();
42
+ if (next === last)
43
+ return;
44
+ last = next;
45
+ listener();
46
+ };
47
+ var stopLanguage = subscribeDisplayLanguage(check);
48
+ var stopSurface = subscribeSurfaceContext(check);
49
+ return function () {
50
+ stopLanguage();
51
+ stopSurface();
52
+ };
53
+ }
package/dist/es5/index.js CHANGED
@@ -7,8 +7,8 @@
7
7
  */
8
8
  export { LingXiaBridge, channel, initBridge, invoke, notify, stream, } from './bridge';
9
9
  export { isNativeError } from './invocation';
10
- export { getDisplayLanguage, subscribeDisplayLanguage } from './runtime-env';
11
- export { renderErrorUI, hasError, getErrorInfo } from './error';
10
+ export { getHost, subscribeHost } from './host';
11
+ export { renderErrorUI, renderPageFault, hasError, getErrorInfo } from './error';
12
12
  export { boot, bootWhenReady } from './boot';
13
13
  import { bootWhenReady } from './boot';
14
14
  import { registerUIHandlers } from './ui';
@@ -61,9 +61,30 @@ function store() {
61
61
  }
62
62
  return window.__lxDisplayLanguage;
63
63
  }
64
+ /** Primary subtags of languages written right to left, for engines without `Intl.Locale#getTextInfo`. */
65
+ var RTL_LANGUAGES = new Set(['ar', 'ckb', 'dv', 'fa', 'he', 'ps', 'sd', 'ug', 'ur', 'yi']);
66
+ /** The writing direction of a BCP-47 tag. */
67
+ export function textDirection(tag) {
68
+ try {
69
+ var locale = new Intl.Locale(tag);
70
+ var info = typeof locale.getTextInfo === 'function' ? locale.getTextInfo() : locale.textInfo;
71
+ if ((info === null || info === void 0 ? void 0 : info.direction) === 'rtl' || (info === null || info === void 0 ? void 0 : info.direction) === 'ltr')
72
+ return info.direction;
73
+ }
74
+ catch (_a) {
75
+ // Not a tag `Intl` accepts; fall through to the subtag list.
76
+ }
77
+ return RTL_LANGUAGES.has(tag.split(/[-_]/)[0].toLowerCase()) ? 'rtl' : 'ltr';
78
+ }
79
+ /**
80
+ * `lang` and `dir` on `<html>` follow the display language, so a page never
81
+ * sets either: CSS logical properties and `:dir()` just work.
82
+ */
64
83
  function stampDocumentLanguage() {
65
84
  if (typeof document !== 'undefined' && document.documentElement) {
66
- document.documentElement.lang = store().value;
85
+ var language = store().value;
86
+ document.documentElement.lang = language;
87
+ document.documentElement.dir = textDirection(language);
67
88
  }
68
89
  }
69
90
  stampDocumentLanguage();
@@ -71,25 +92,37 @@ stampDocumentLanguage();
71
92
  * Host entry point for a language the user changed while this document was
72
93
  * open. Bootstrap alone would leave a live page in the language it started in,
73
94
  * with the native chrome around it already switched.
95
+ *
96
+ * The host pushes each change once, and again — the current value — when a
97
+ * document's bridge reports ready, since a change made while it loaded found
98
+ * no hook to call. Those two can land in either order, so a push carries the
99
+ * host revision it took effect at and an older one never overwrites a newer
100
+ * one. A push without a revision applies as it always did.
74
101
  */
75
- function applyDisplayLanguage(next) {
102
+ function applyDisplayLanguage(next, revision) {
76
103
  var e_1, _a;
104
+ var _b;
77
105
  var normalized = typeof next === 'string' ? next.trim() : '';
78
106
  var current = store();
107
+ if (typeof revision === 'number' && Number.isFinite(revision)) {
108
+ if (revision <= ((_b = current.revision) !== null && _b !== void 0 ? _b : 0))
109
+ return;
110
+ current.revision = revision;
111
+ }
79
112
  if (!normalized || normalized === current.value)
80
113
  return;
81
114
  current.value = normalized;
82
115
  stampDocumentLanguage();
83
116
  try {
84
- for (var _b = __values(__spreadArray([], __read(current.listeners), false)), _c = _b.next(); !_c.done; _c = _b.next()) {
85
- var listener = _c.value;
117
+ for (var _c = __values(__spreadArray([], __read(current.listeners), false)), _d = _c.next(); !_d.done; _d = _c.next()) {
118
+ var listener = _d.value;
86
119
  listener(normalized);
87
120
  }
88
121
  }
89
122
  catch (e_1_1) { e_1 = { error: e_1_1 }; }
90
123
  finally {
91
124
  try {
92
- if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
125
+ if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
93
126
  }
94
127
  finally { if (e_1) throw e_1.error; }
95
128
  }