@lingxia/page-runtime 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.
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/page-chrome.d.ts +0 -5
- package/dist/page-chrome.js +7 -17
- package/dist/shared/runtime.d.ts +42 -4
- package/dist/shared/runtime.js +173 -51
- package/dist/shared/startup.d.ts +2 -0
- package/dist/shared/startup.js +25 -0
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { ensurePageBridgeSubscription, getPageActions, getPageSnapshot,
|
|
2
|
-
export {
|
|
1
|
+
export { ensurePageBridgeSubscription, getPageActions, getPageSnapshot, isPageReady, subscribePageSnapshot, whenPageReady, type ActionMap, type PageActions, type DeepReadonly, type Snapshot, } from "./shared/runtime.js";
|
|
2
|
+
export { waitForPageState } from "./shared/startup.js";
|
|
3
|
+
export { installPageChromeRuntime, type LxPageChrome, type PageChromeLayoutSnapshot, type PageChromeRect, } from "./page-chrome.js";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { ensurePageBridgeSubscription, getPageActions, getPageSnapshot,
|
|
2
|
-
export {
|
|
1
|
+
export { ensurePageBridgeSubscription, getPageActions, getPageSnapshot, isPageReady, subscribePageSnapshot, whenPageReady, } from "./shared/runtime.js";
|
|
2
|
+
export { waitForPageState } from "./shared/startup.js";
|
|
3
|
+
export { installPageChromeRuntime, } from "./page-chrome.js";
|
package/dist/page-chrome.d.ts
CHANGED
|
@@ -22,7 +22,6 @@ export interface PageChromeLayoutSnapshot {
|
|
|
22
22
|
export interface LxPageChrome {
|
|
23
23
|
readonly layout: PageChromeLayoutSnapshot;
|
|
24
24
|
}
|
|
25
|
-
export type PageChromeLayoutListener = (layout: PageChromeLayoutSnapshot) => void;
|
|
26
25
|
declare global {
|
|
27
26
|
interface Window {
|
|
28
27
|
readonly lxPageChrome: LxPageChrome;
|
|
@@ -32,10 +31,6 @@ declare global {
|
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
33
|
export declare function shouldApplyPageChromeRevision(currentRevision: number, nextRevision: number): boolean;
|
|
35
|
-
/** Read the latest realized page-chrome layout synchronously. */
|
|
36
|
-
export declare function getPageChromeLayout(): PageChromeLayoutSnapshot;
|
|
37
|
-
/** Subscribe to realized page-chrome layout changes. */
|
|
38
|
-
export declare function subscribePageChromeLayout(listener: PageChromeLayoutListener): () => void;
|
|
39
34
|
/** Ensure browser previews have the same synchronous contract as native pages. */
|
|
40
35
|
export declare function installPageChromeRuntime(): LxPageChrome | undefined;
|
|
41
36
|
declare global {
|
package/dist/page-chrome.js
CHANGED
|
@@ -13,23 +13,13 @@ function projectPageChromeLayout(layout) {
|
|
|
13
13
|
root?.style.setProperty("--lx-page-chrome-top-inset", `${layout.topInset}px`);
|
|
14
14
|
root?.style.setProperty("--lx-page-chrome-bottom-inset", `${layout.bottomInset}px`);
|
|
15
15
|
root?.style.setProperty("--lx-page-chrome-capsule-inline-end-inset", `${layout.capsuleInlineEndInset}px`);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
/** Subscribe to realized page-chrome layout changes. */
|
|
24
|
-
export function subscribePageChromeLayout(listener) {
|
|
25
|
-
if (typeof window === "undefined")
|
|
26
|
-
return () => { };
|
|
27
|
-
installPageChromeRuntime();
|
|
28
|
-
const handleChange = (event) => {
|
|
29
|
-
listener(event.detail);
|
|
30
|
-
};
|
|
31
|
-
window.addEventListener("lxpagechromechange", handleChange);
|
|
32
|
-
return () => window.removeEventListener("lxpagechromechange", handleChange);
|
|
16
|
+
// The capsule's box, so a header can clear it in CSS alone
|
|
17
|
+
// (`padding-top: calc(var(--lx-page-chrome-capsule-bottom) + 12px)`).
|
|
18
|
+
// All zero when the page has no capsule.
|
|
19
|
+
const capsule = layout.capsuleRect;
|
|
20
|
+
for (const edge of ["top", "right", "bottom", "left", "width", "height"]) {
|
|
21
|
+
root?.style.setProperty(`--lx-page-chrome-capsule-${edge}`, `${capsule ? capsule[edge] : 0}px`);
|
|
22
|
+
}
|
|
33
23
|
}
|
|
34
24
|
/** Ensure browser previews have the same synchronous contract as native pages. */
|
|
35
25
|
export function installPageChromeRuntime() {
|
package/dist/shared/runtime.d.ts
CHANGED
|
@@ -1,10 +1,48 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type LxStream } from "@lingxia/bridge";
|
|
2
2
|
export type ActionMap = Record<string, (...args: never[]) => unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* The View's side of a page action: at most one JSON payload; unary actions
|
|
5
|
+
* acknowledge completion, generators arrive as a stream handle.
|
|
6
|
+
*/
|
|
7
|
+
export type PageActions<A extends ActionMap> = {
|
|
8
|
+
readonly [K in keyof A]: A[K] extends (...args: infer P) => infer R ? number extends P["length"] ? (payload?: unknown) => ViewActionResult<R> : P extends [] | [unknown] | [unknown?] ? (...args: P) => ViewActionResult<R> : (error: "a page action takes at most one JSON payload") => never : never;
|
|
9
|
+
};
|
|
10
|
+
type ViewActionResult<R> = R extends LxStream<any, any> ? R : R extends AsyncGenerator<infer T, infer TReturn, any> ? LxStream<T, TReturn> : Promise<Awaited<R>>;
|
|
3
11
|
export type Snapshot = Record<string, unknown>;
|
|
12
|
+
/**
|
|
13
|
+
* What a View reads of its page's data: Logic owns it, so every level is
|
|
14
|
+
* readonly. Keep editable state (a form draft) in the View and send it with
|
|
15
|
+
* an action.
|
|
16
|
+
*/
|
|
17
|
+
export type DeepReadonly<T> = T extends (...args: never[]) => unknown ? T : T extends object ? {
|
|
18
|
+
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
|
19
|
+
} : T;
|
|
4
20
|
export type Listener = () => void;
|
|
5
21
|
export declare function ensurePageBridgeSubscription(): void;
|
|
6
22
|
export declare function subscribePageSnapshot(listener: Listener): () => void;
|
|
7
|
-
|
|
23
|
+
/** Whether the page's first state has arrived. Flips once per document. */
|
|
24
|
+
export declare function isPageReady(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Resolves once the page's first state has arrived — the host pushes it at
|
|
27
|
+
* bridge-ready, before `onLoad`, carrying `Page({ data })`'s defaults — so a
|
|
28
|
+
* page can mount with its data whole. With `timeoutMs`, rejects if Logic has
|
|
29
|
+
* not delivered it by then (it failed to load, or threw first); `null` waits
|
|
30
|
+
* without limit.
|
|
31
|
+
*/
|
|
32
|
+
export declare function whenPageReady(options?: {
|
|
33
|
+
timeoutMs?: number | null;
|
|
34
|
+
}): Promise<void>;
|
|
8
35
|
export declare function getPageSnapshot<TData = Snapshot>(): TData;
|
|
9
|
-
|
|
10
|
-
|
|
36
|
+
/**
|
|
37
|
+
* The page's actions: one object for the whole page, so it is a stable
|
|
38
|
+
* dependency. Built on first use — the page's action names arrive with its
|
|
39
|
+
* bridge metadata, after the page module has evaluated.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getPageActions<TActions extends ActionMap>(): PageActions<TActions>;
|
|
42
|
+
declare global {
|
|
43
|
+
interface Window {
|
|
44
|
+
/** Automation-only; see `invokePageActionForAutomation`. */
|
|
45
|
+
__lxInvokePageAction?: (name: string, payload?: unknown) => Promise<unknown>;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export {};
|
package/dist/shared/runtime.js
CHANGED
|
@@ -2,7 +2,16 @@ let snapshot = {};
|
|
|
2
2
|
let stateInfo = { rev: -1, initial: true };
|
|
3
3
|
let subscribed = false;
|
|
4
4
|
let subscribeRetryTimer = null;
|
|
5
|
+
let snapshotRetryTimer = null;
|
|
6
|
+
let snapshotRetries = 0;
|
|
7
|
+
/**
|
|
8
|
+
* The host pushes a page's first state at bridge-ready, so the request is only
|
|
9
|
+
* a fallback: a few tries, backing off, then stop. A page with no Logic never
|
|
10
|
+
* answers, and must not be asked for the life of the page.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_SNAPSHOT_RETRIES = 3;
|
|
5
13
|
let initialSnapshotResolved = false;
|
|
14
|
+
let actions = null;
|
|
6
15
|
let snapshotRequestInFlight = false;
|
|
7
16
|
const listeners = new Set();
|
|
8
17
|
function notifyListeners() {
|
|
@@ -15,8 +24,26 @@ function notifyListeners() {
|
|
|
15
24
|
}
|
|
16
25
|
});
|
|
17
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* In a dev session the page data is frozen, so a View that writes to it fails
|
|
29
|
+
* at the write instead of drifting from Logic until the next update. Each
|
|
30
|
+
* push is a fresh copy, so nothing else holds these objects.
|
|
31
|
+
*/
|
|
32
|
+
function isDevSession() {
|
|
33
|
+
return typeof window !== "undefined" && window.__LX_BRIDGE_CFG?.dev === true;
|
|
34
|
+
}
|
|
35
|
+
function deepFreeze(value) {
|
|
36
|
+
if (!value || typeof value !== "object" || Object.isFrozen(value))
|
|
37
|
+
return;
|
|
38
|
+
Object.freeze(value);
|
|
39
|
+
for (const key of Object.keys(value)) {
|
|
40
|
+
deepFreeze(value[key]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
18
43
|
function updateSnapshot(next, info) {
|
|
19
44
|
snapshot = next && typeof next === "object" ? next : {};
|
|
45
|
+
if (isDevSession())
|
|
46
|
+
deepFreeze(snapshot);
|
|
20
47
|
stateInfo = info;
|
|
21
48
|
notifyListeners();
|
|
22
49
|
}
|
|
@@ -28,11 +55,29 @@ function scheduleSubscribeRetry() {
|
|
|
28
55
|
ensurePageBridgeSubscription();
|
|
29
56
|
}, 10);
|
|
30
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Retry the snapshot request itself. The subscription is already in place, so
|
|
60
|
+
* retrying it (as this used to) returned at once and never asked again; the
|
|
61
|
+
* page then relied on the host's own push at bridge-ready.
|
|
62
|
+
*/
|
|
63
|
+
function scheduleSnapshotRetry() {
|
|
64
|
+
if (snapshotRetryTimer !== null || snapshotRetries >= MAX_SNAPSHOT_RETRIES)
|
|
65
|
+
return;
|
|
66
|
+
snapshotRetries += 1;
|
|
67
|
+
snapshotRetryTimer = setTimeout(() => {
|
|
68
|
+
snapshotRetryTimer = null;
|
|
69
|
+
requestInitialSnapshot(window.LingXiaBridge);
|
|
70
|
+
}, 250 * 2 ** (snapshotRetries - 1));
|
|
71
|
+
}
|
|
31
72
|
function requestInitialSnapshot(bridge) {
|
|
73
|
+
if (stateInfo.rev >= 0)
|
|
74
|
+
initialSnapshotResolved = true;
|
|
32
75
|
if (initialSnapshotResolved || snapshotRequestInFlight)
|
|
33
76
|
return;
|
|
34
|
-
if (!bridge?.raw?.call)
|
|
77
|
+
if (!bridge?.raw?.call) {
|
|
78
|
+
scheduleSnapshotRetry();
|
|
35
79
|
return;
|
|
80
|
+
}
|
|
36
81
|
snapshotRequestInFlight = true;
|
|
37
82
|
bridge
|
|
38
83
|
.raw.call("state.getSnapshot", { scope: "page" })
|
|
@@ -40,7 +85,7 @@ function requestInitialSnapshot(bridge) {
|
|
|
40
85
|
initialSnapshotResolved = true;
|
|
41
86
|
})
|
|
42
87
|
.catch(() => {
|
|
43
|
-
|
|
88
|
+
scheduleSnapshotRetry();
|
|
44
89
|
})
|
|
45
90
|
.finally(() => {
|
|
46
91
|
snapshotRequestInFlight = false;
|
|
@@ -68,40 +113,69 @@ export function subscribePageSnapshot(listener) {
|
|
|
68
113
|
listeners.delete(listener);
|
|
69
114
|
};
|
|
70
115
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return () => { };
|
|
116
|
+
/** Whether the page's first state has arrived. Flips once per document. */
|
|
117
|
+
export function isPageReady() {
|
|
74
118
|
ensurePageBridgeSubscription();
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
119
|
+
return stateInfo.rev >= 0;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolves once the page's first state has arrived — the host pushes it at
|
|
123
|
+
* bridge-ready, before `onLoad`, carrying `Page({ data })`'s defaults — so a
|
|
124
|
+
* page can mount with its data whole. With `timeoutMs`, rejects if Logic has
|
|
125
|
+
* not delivered it by then (it failed to load, or threw first); `null` waits
|
|
126
|
+
* without limit.
|
|
127
|
+
*/
|
|
128
|
+
export function whenPageReady(options = {}) {
|
|
129
|
+
if (isPageReady())
|
|
130
|
+
return Promise.resolve();
|
|
131
|
+
const timeoutMs = options.timeoutMs === undefined ? 10000 : options.timeoutMs;
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
let timer = null;
|
|
134
|
+
const check = () => {
|
|
135
|
+
if (stateInfo.rev < 0)
|
|
136
|
+
return;
|
|
137
|
+
if (timer !== null)
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
listeners.delete(check);
|
|
140
|
+
resolve();
|
|
141
|
+
};
|
|
142
|
+
if (timeoutMs !== null) {
|
|
143
|
+
timer = setTimeout(() => {
|
|
144
|
+
listeners.delete(check);
|
|
145
|
+
reject(new Error(`Page Logic did not deliver the page state within ${timeoutMs} ms`));
|
|
146
|
+
}, timeoutMs);
|
|
147
|
+
}
|
|
148
|
+
listeners.add(check);
|
|
80
149
|
});
|
|
81
150
|
}
|
|
82
151
|
export function getPageSnapshot() {
|
|
83
152
|
ensurePageBridgeSubscription();
|
|
84
153
|
return snapshot;
|
|
85
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* The page's actions: one object for the whole page, so it is a stable
|
|
157
|
+
* dependency. Built on first use — the page's action names arrive with its
|
|
158
|
+
* bridge metadata, after the page module has evaluated.
|
|
159
|
+
*/
|
|
86
160
|
export function getPageActions() {
|
|
87
|
-
|
|
161
|
+
if (actions)
|
|
162
|
+
return actions;
|
|
88
163
|
const bridge = window.__pageBridge;
|
|
89
164
|
if (!bridge?.__names) {
|
|
90
|
-
|
|
165
|
+
throw new Error("Page actions are not ready. Read useLxPage() during component setup/render, or getPage() after pageReady(); pass actions into shared helpers.");
|
|
91
166
|
}
|
|
167
|
+
const built = {};
|
|
92
168
|
for (const name of bridge.__names) {
|
|
93
169
|
if (typeof name !== "string")
|
|
94
170
|
continue;
|
|
95
171
|
const fn = getOrCreatePageAction(bridge, name);
|
|
96
172
|
if (typeof fn === "function") {
|
|
97
|
-
|
|
173
|
+
built[name] = fn;
|
|
98
174
|
}
|
|
99
175
|
}
|
|
176
|
+
actions = built;
|
|
100
177
|
return actions;
|
|
101
178
|
}
|
|
102
|
-
export function getPageStateInfo() {
|
|
103
|
-
return stateInfo;
|
|
104
|
-
}
|
|
105
179
|
function getOrCreatePageAction(bridge, name) {
|
|
106
180
|
const existing = bridge[name];
|
|
107
181
|
if (typeof existing === "function") {
|
|
@@ -114,7 +188,9 @@ function getOrCreatePageAction(bridge, name) {
|
|
|
114
188
|
}
|
|
115
189
|
function resolvePageActionMode(bridge, name) {
|
|
116
190
|
const mode = bridge.__modes?.[name];
|
|
117
|
-
|
|
191
|
+
if (mode === "call" || mode === "stream")
|
|
192
|
+
return mode;
|
|
193
|
+
throw new Error(`Invalid bridge mode for page action '${name}'; regenerate the page with the current CLI`);
|
|
118
194
|
}
|
|
119
195
|
function definePageBridgeAction(name, mode) {
|
|
120
196
|
function action(...args) {
|
|
@@ -133,7 +209,7 @@ function definePageBridgeAction(name, mode) {
|
|
|
133
209
|
return handle;
|
|
134
210
|
}
|
|
135
211
|
if (mode === "call") {
|
|
136
|
-
const promise = bridge
|
|
212
|
+
const promise = callUnaryPageAction(bridge, name, payload);
|
|
137
213
|
if (promise && typeof promise.catch === "function") {
|
|
138
214
|
promise.catch((err) => {
|
|
139
215
|
console.warn(`[PageFunc] ${name} failed:`, err instanceof Error ? err.message : err);
|
|
@@ -141,8 +217,7 @@ function definePageBridgeAction(name, mode) {
|
|
|
141
217
|
}
|
|
142
218
|
return promise;
|
|
143
219
|
}
|
|
144
|
-
bridge
|
|
145
|
-
return undefined;
|
|
220
|
+
throw new Error(`Invalid bridge mode for page action '${name}'`);
|
|
146
221
|
}
|
|
147
222
|
Object.assign(action, {
|
|
148
223
|
__logicFunc: true,
|
|
@@ -151,40 +226,87 @@ function definePageBridgeAction(name, mode) {
|
|
|
151
226
|
});
|
|
152
227
|
return action;
|
|
153
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* The one wire path of a unary action. It has no bridge deadline: an action
|
|
231
|
+
* may wait for a person or poll for minutes, and settles when Logic settles
|
|
232
|
+
* or the document goes away.
|
|
233
|
+
*/
|
|
234
|
+
async function callUnaryPageAction(bridge, name, payload, dropWhenGone = true) {
|
|
235
|
+
// Native components fire events before the page's first state arrives,
|
|
236
|
+
// and the host refuses calls until then: hold the call instead.
|
|
237
|
+
if (!isPageReady())
|
|
238
|
+
await whenPageReady({ timeoutMs: null });
|
|
239
|
+
try {
|
|
240
|
+
return await bridge.raw.call(name, payload, { timeoutMs: 0 });
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
// Once the page has had its state, a not-ready answer means the host
|
|
244
|
+
// ended this document's session: the page has left, and a native
|
|
245
|
+
// component's late event has no one to report to. Drop it quietly.
|
|
246
|
+
// Automation has a caller waiting, so it gets the error.
|
|
247
|
+
if (dropWhenGone && error?.code === "BRIDGE_NOT_READY") {
|
|
248
|
+
return new Promise(() => { });
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* A DOM or framework event, in this realm or another. A JSON payload never
|
|
255
|
+
* carries methods, so event methods tell the two apart.
|
|
256
|
+
*/
|
|
257
|
+
function isEventLike(value) {
|
|
258
|
+
if (typeof Event !== "undefined" && value instanceof Event)
|
|
259
|
+
return true;
|
|
260
|
+
if (!value || typeof value !== "object")
|
|
261
|
+
return false;
|
|
262
|
+
const event = value;
|
|
263
|
+
return typeof event.preventDefault === "function" && typeof event.stopPropagation === "function";
|
|
264
|
+
}
|
|
265
|
+
const PAGE_ACTIONS_NOT_READY = "PAGE_ACTIONS_NOT_READY";
|
|
266
|
+
function pageActionError(code, message) {
|
|
267
|
+
return Object.assign(new Error(message), { code });
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Automation entry (`PageDriver.action`): invoke one unary action of this
|
|
271
|
+
* page with a JSON payload, as the View would. The payload is passed as is —
|
|
272
|
+
* no DOM-event repackaging.
|
|
273
|
+
*/
|
|
274
|
+
function invokePageActionForAutomation(name, payload) {
|
|
275
|
+
if (typeof name !== "string" || name === "") {
|
|
276
|
+
return Promise.reject(pageActionError("BRIDGE_MALFORMED_MESSAGE", "Page action name must be a non-empty string"));
|
|
277
|
+
}
|
|
278
|
+
const metadata = window.__pageBridge;
|
|
279
|
+
const bridge = window.LingXiaBridge;
|
|
280
|
+
if (!Array.isArray(metadata?.__names) || !bridge?.raw) {
|
|
281
|
+
return Promise.reject(pageActionError(PAGE_ACTIONS_NOT_READY, "Page actions are not ready yet"));
|
|
282
|
+
}
|
|
283
|
+
if (!metadata.__names.includes(name)) {
|
|
284
|
+
const known = metadata.__names.length ? metadata.__names.join(", ") : "none";
|
|
285
|
+
return Promise.reject(pageActionError("BRIDGE_METHOD_NOT_FOUND", `'${name}' is not an action of this page (actions: ${known})`));
|
|
286
|
+
}
|
|
287
|
+
if (metadata.__modes?.[name] !== "call") {
|
|
288
|
+
return Promise.reject(pageActionError("PAGE_ACTION_NOT_UNARY", `Page action '${name}' is a stream action; only unary actions can be invoked`));
|
|
289
|
+
}
|
|
290
|
+
return callUnaryPageAction(bridge, name, payload, false);
|
|
291
|
+
}
|
|
292
|
+
if (typeof window !== "undefined") {
|
|
293
|
+
Object.defineProperty(window, "__lxInvokePageAction", {
|
|
294
|
+
value: invokePageActionForAutomation,
|
|
295
|
+
configurable: true,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
154
298
|
function filterPayload(name, args) {
|
|
155
299
|
const clean = [];
|
|
156
300
|
for (const value of args) {
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
// Some framework wrappers / WebView realms do not preserve
|
|
169
|
-
// `instanceof CustomEvent`, but still expose the portable event payload
|
|
170
|
-
// shape. Keep it before the generic Event stripping path so page actions
|
|
171
|
-
// receive `event.detail` consistently.
|
|
172
|
-
const maybeEvent = value;
|
|
173
|
-
if (maybeEvent && typeof maybeEvent === "object" && typeof maybeEvent.type === "string" && "detail" in maybeEvent) {
|
|
174
|
-
clean.push({
|
|
175
|
-
type: maybeEvent.type,
|
|
176
|
-
detail: maybeEvent.detail,
|
|
177
|
-
});
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
// Generic Event / event-like objects with non-serializable methods stay
|
|
181
|
-
// stripped — there's no portable payload to extract.
|
|
182
|
-
if (value instanceof Event)
|
|
183
|
-
continue;
|
|
184
|
-
if (value &&
|
|
185
|
-
typeof value === "object" &&
|
|
186
|
-
"stopPropagation" in value &&
|
|
187
|
-
typeof value.stopPropagation === "function") {
|
|
301
|
+
// A DOM event bound straight to an action (`onVideoEnded={action}`)
|
|
302
|
+
// crosses as its portable `{ type, detail }`; one without `detail`
|
|
303
|
+
// carries nothing portable. A business payload passes as is, whatever
|
|
304
|
+
// its field names.
|
|
305
|
+
if (isEventLike(value)) {
|
|
306
|
+
const event = value;
|
|
307
|
+
if (typeof event.type === "string" && "detail" in event) {
|
|
308
|
+
clean.push({ type: event.type, detail: event.detail });
|
|
309
|
+
}
|
|
188
310
|
continue;
|
|
189
311
|
}
|
|
190
312
|
clean.push(value);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { renderPageFault } from "@lingxia/bridge";
|
|
2
|
+
import { isPageReady, whenPageReady } from "./runtime.js";
|
|
3
|
+
let pending;
|
|
4
|
+
/** Mount with complete initial data; a delayed startup stays recoverable. */
|
|
5
|
+
export function waitForPageState() {
|
|
6
|
+
if (isPageReady())
|
|
7
|
+
return Promise.resolve();
|
|
8
|
+
return pending ?? (pending = wait().finally(() => { pending = undefined; }));
|
|
9
|
+
}
|
|
10
|
+
async function wait() {
|
|
11
|
+
const delay = 10000;
|
|
12
|
+
const panel = {};
|
|
13
|
+
const timer = setTimeout(() => {
|
|
14
|
+
const reason = `Page Logic has not delivered the page state after ${delay} ms`;
|
|
15
|
+
console.error(`[LingXia] ${location.pathname}: ${reason}`);
|
|
16
|
+
panel.dismiss = renderPageFault(location.pathname, reason);
|
|
17
|
+
}, delay);
|
|
18
|
+
try {
|
|
19
|
+
await whenPageReady({ timeoutMs: null });
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
panel.dismiss?.();
|
|
24
|
+
}
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lingxia/page-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared page runtime core for LingXia lxapp frameworks",
|
|
6
6
|
"type": "module",
|
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
"build": "npm run clean && npm --prefix ../lingxia-bridge run build:types && tsc -p tsconfig.json",
|
|
18
18
|
"clean": "rm -rf dist",
|
|
19
19
|
"test:runtime": "npm run build && node tests/page-chrome-revision.mjs",
|
|
20
|
-
"prepublishOnly": "npm run build"
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"test": "rolldown src/shared/runtime.ts --file dist/test/runtime.mjs --format esm && node ./test-support/test-runtime.mjs"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"@lingxia/bridge": "~0.
|
|
24
|
+
"@lingxia/bridge": "~0.19.0"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@lingxia/bridge": "file:../lingxia-bridge",
|