@netless/app-presentation 0.1.13-beta.2 → 0.1.13-beta.3
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/README-zh.md +18 -0
- package/README.md +22 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.global.js +81 -48
- package/dist/index.js +81 -48
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +81 -48
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/app-presentation.ts +9 -41
- package/src/preload.ts +23 -11
- package/src/runtime-lifecycle.ts +13 -6
- package/src/setup-ready.ts +39 -0
package/src/app-presentation.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
isValidSize,
|
|
37
37
|
} from "./camera-reference";
|
|
38
38
|
import { createPresentationRuntimeTeardown } from "./runtime-lifecycle";
|
|
39
|
+
import { waitForCurrentPageImage } from "./setup-ready";
|
|
39
40
|
import debounce from "lodash/debounce";
|
|
40
41
|
|
|
41
42
|
export type Logger = (...data: any[]) => void;
|
|
@@ -199,44 +200,6 @@ const isBlurDegradationAllowed = (context: AppContext): boolean => {
|
|
|
199
200
|
|
|
200
201
|
const DEFAULT_SETUP_READY_TIMEOUT = 5_000;
|
|
201
202
|
|
|
202
|
-
/**
|
|
203
|
-
* Resolve once the current page background image has loaded (or after
|
|
204
|
-
* `timeoutMs`). `false` means timeout or the app was disposed mid-wait;
|
|
205
|
-
* callers must treat it as "continue loading in background", not an error.
|
|
206
|
-
*/
|
|
207
|
-
const waitForCurrentPageImage = (
|
|
208
|
-
view: View,
|
|
209
|
-
timeoutMs: number,
|
|
210
|
-
isDisposed: () => boolean,
|
|
211
|
-
): Promise<boolean> => {
|
|
212
|
-
return new Promise<boolean>(resolve => {
|
|
213
|
-
let settled = false;
|
|
214
|
-
let pollTimer: number | undefined;
|
|
215
|
-
const settle = (loaded: boolean) => {
|
|
216
|
-
if (settled) return;
|
|
217
|
-
settled = true;
|
|
218
|
-
window.clearTimeout(timeoutTimer);
|
|
219
|
-
if (pollTimer !== undefined) window.clearInterval(pollTimer);
|
|
220
|
-
resolve(loaded);
|
|
221
|
-
};
|
|
222
|
-
const timeoutTimer = window.setTimeout(() => settle(false), timeoutMs);
|
|
223
|
-
const check = () => {
|
|
224
|
-
if (isDisposed()) {
|
|
225
|
-
settle(false);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
const img = findBackgroundImage(view);
|
|
229
|
-
if (img && img.complete && img.naturalWidth > 0) {
|
|
230
|
-
settle(true);
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
check();
|
|
234
|
-
if (!settled) {
|
|
235
|
-
pollTimer = window.setInterval(check, 100);
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
};
|
|
239
|
-
|
|
240
203
|
/**
|
|
241
204
|
* Compare image URLs by resolved absolute href so relative/absolute
|
|
242
205
|
* representations of the same URL do not look different (and to keep the
|
|
@@ -283,7 +246,8 @@ const hasUsableThumbnail = (page: PresentationPage): boolean => {
|
|
|
283
246
|
* hidden and replaced with a static pixel after TeleBox commits the blur.
|
|
284
247
|
*
|
|
285
248
|
* While degraded, every load of a background `<img>` is re-degraded
|
|
286
|
-
* immediately via a capture-phase `load` listener on the
|
|
249
|
+
* immediately via a capture-phase `load` listener on the document, filtered
|
|
250
|
+
* to the current view container
|
|
287
251
|
* (`load` does not bubble but capture works). This covers remote page turns,
|
|
288
252
|
* SDK React re-renders and the SDK's own background-image onError retries —
|
|
289
253
|
* any path that (re)renders the full-size image while the app stays blurred.
|
|
@@ -1522,9 +1486,13 @@ export const NetlessAppPresentation: NetlessApp<
|
|
|
1522
1486
|
return prepareScenesPromise.then(async () => {
|
|
1523
1487
|
// Keep the serial setup queue honest: resolve only once the current
|
|
1524
1488
|
// page image is actually visible (or the bounded wait gives up).
|
|
1525
|
-
const
|
|
1526
|
-
|
|
1489
|
+
const result = await waitForCurrentPageImage(
|
|
1490
|
+
() => findBackgroundImage(view), setupReadyTimeout, () => disposed,
|
|
1491
|
+
);
|
|
1492
|
+
if (result === "timeout" && !disposed) {
|
|
1527
1493
|
warn(`[Presentation] setup ready wait timed out after ${setupReadyTimeout}ms`);
|
|
1494
|
+
} else if (result === "error" && !disposed) {
|
|
1495
|
+
warn("[Presentation] setup page image failed to load; continuing setup");
|
|
1528
1496
|
}
|
|
1529
1497
|
return controller;
|
|
1530
1498
|
}) as unknown as PresentationController;
|
package/src/preload.ts
CHANGED
|
@@ -38,6 +38,7 @@ export class Preload implements IDisposable {
|
|
|
38
38
|
// 暂停后不再创建预加载链接(app 失焦降级时调用);已插入的 link 会被移除,
|
|
39
39
|
// 挂起的 idle 回调重新进入 touch() 时也会被跳过
|
|
40
40
|
paused: boolean = false;
|
|
41
|
+
private disposed = false;
|
|
41
42
|
constructor(readonly pages: PresentationPage[]) {
|
|
42
43
|
this.preloadMap = new Map(pages.map((e,index) => [index, {
|
|
43
44
|
src: e.src,
|
|
@@ -52,23 +53,23 @@ export class Preload implements IDisposable {
|
|
|
52
53
|
this.destroySomeLink();
|
|
53
54
|
}
|
|
54
55
|
resume(index?: number) {
|
|
55
|
-
if (!this.paused) return;
|
|
56
|
+
if (!this.paused || this.disposed) return;
|
|
56
57
|
this.paused = false;
|
|
57
58
|
this.touch(index ?? this.touchIndex, true);
|
|
58
59
|
}
|
|
59
60
|
touch(index: number, force: boolean = false) {
|
|
60
|
-
if (this.paused) return;
|
|
61
|
+
if (this.paused || this.disposed) return;
|
|
61
62
|
if (index >= this.preloadSize) {
|
|
62
63
|
this.touchIndex = 0;
|
|
63
64
|
} else {
|
|
64
65
|
this.touchIndex = index;
|
|
65
66
|
}
|
|
66
67
|
const value = this.preloadMap.get(this.touchIndex);
|
|
67
|
-
if (value && value.state === ELoadState.unloaded) {
|
|
68
|
+
if (value && (value.state === ELoadState.unloaded || (force && value.state === ELoadState.error))) {
|
|
68
69
|
this.createLink(this.touchIndex, force);
|
|
69
70
|
}
|
|
70
71
|
if (this.loadingLinks.size < Preload.maxLinks) {
|
|
71
|
-
const willLoad = [...this.preloadMap.entries()].find(([i, e]) => i > this.touchIndex && e.state
|
|
72
|
+
const willLoad = [...this.preloadMap.entries()].find(([i, e]) => i > this.touchIndex && e.state === ELoadState.unloaded);
|
|
72
73
|
if (willLoad) {
|
|
73
74
|
this.requestAsyncCallBack(()=>{this.touch(willLoad[0], false)}, 100);
|
|
74
75
|
}
|
|
@@ -100,21 +101,25 @@ export class Preload implements IDisposable {
|
|
|
100
101
|
linkDom.as = 'image';
|
|
101
102
|
linkDom.href = value.src;
|
|
102
103
|
linkDom.dataset.order = index + '';
|
|
103
|
-
document.head.appendChild(linkDom);
|
|
104
104
|
linkDom.onload = () => {
|
|
105
|
+
if (this.loadingLinks.get(index)?.link !== linkDom) return;
|
|
105
106
|
const value = this.preloadMap.get(index);
|
|
106
107
|
if (value) {
|
|
107
108
|
value.state = ELoadState.loaded;
|
|
108
109
|
this.preloadMap.set(index, value);
|
|
109
|
-
|
|
110
|
-
this.loadingLinks.delete(index);
|
|
110
|
+
this.removeLink(index, linkDom);
|
|
111
111
|
this.touch(this.touchIndex + 1, false);
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
linkDom.onerror = () => {
|
|
115
|
+
if (this.loadingLinks.get(index)?.link !== linkDom) return;
|
|
115
116
|
const value = this.preloadMap.get(index);
|
|
116
|
-
if (value
|
|
117
|
-
|
|
117
|
+
if (value) {
|
|
118
|
+
// Preloading is best effort. Do not retry in an error loop;
|
|
119
|
+
// revisiting the page explicitly may retry after network recovery.
|
|
120
|
+
value.state = ELoadState.error;
|
|
121
|
+
this.removeLink(index, linkDom);
|
|
122
|
+
this.touch(this.touchIndex + 1, false);
|
|
118
123
|
}
|
|
119
124
|
}
|
|
120
125
|
this.loadingLinks.set(index, {
|
|
@@ -123,18 +128,24 @@ export class Preload implements IDisposable {
|
|
|
123
128
|
});
|
|
124
129
|
value.state = ELoadState.unloaded;
|
|
125
130
|
this.preloadMap.set(index, value);
|
|
131
|
+
document.head.appendChild(linkDom);
|
|
126
132
|
return ELoadState.unloaded;
|
|
127
133
|
}
|
|
128
134
|
return ELoadState.error;
|
|
129
135
|
}
|
|
130
136
|
|
|
137
|
+
private removeLink(index: number, link: HTMLLinkElement) {
|
|
138
|
+
link.onload = null;
|
|
139
|
+
link.onerror = null;
|
|
140
|
+
document.head.contains(link) && document.head.removeChild(link);
|
|
141
|
+
this.loadingLinks.delete(index);
|
|
142
|
+
}
|
|
131
143
|
private destroySomeLink(excludeIndex?: number) {
|
|
132
144
|
for (const [index, { link }] of this.loadingLinks) {
|
|
133
145
|
if (excludeIndex !== undefined && index === excludeIndex) {
|
|
134
146
|
continue;
|
|
135
147
|
}
|
|
136
|
-
|
|
137
|
-
this.loadingLinks.delete(index);
|
|
148
|
+
this.removeLink(index, link);
|
|
138
149
|
}
|
|
139
150
|
}
|
|
140
151
|
private async requestAsyncCallBack (callBack:()=>void, timeout:number):Promise<void> {
|
|
@@ -152,6 +163,7 @@ export class Preload implements IDisposable {
|
|
|
152
163
|
callBack();
|
|
153
164
|
}
|
|
154
165
|
dispose() {
|
|
166
|
+
this.disposed = true;
|
|
155
167
|
this.destroySomeLink();
|
|
156
168
|
this.preloadMap.clear();
|
|
157
169
|
}
|
package/src/runtime-lifecycle.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface PresentationRuntimeBox {
|
|
2
|
-
unmountContent(): unknown;
|
|
3
|
-
unmountFooter(): unknown;
|
|
4
|
-
unmountStyles(): unknown;
|
|
2
|
+
unmountContent?(): unknown;
|
|
3
|
+
unmountFooter?(): unknown;
|
|
4
|
+
unmountStyles?(): unknown;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
/** Build an idempotent runtime teardown while preserving the TeleBox shell. */
|
|
@@ -16,9 +16,16 @@ export const createPresentationRuntimeTeardown = (
|
|
|
16
16
|
try {
|
|
17
17
|
dispose();
|
|
18
18
|
} finally {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
// A partial host API or a failing mount must not skip other cleanup.
|
|
20
|
+
try {
|
|
21
|
+
box.unmountContent?.();
|
|
22
|
+
} finally {
|
|
23
|
+
try {
|
|
24
|
+
box.unmountFooter?.();
|
|
25
|
+
} finally {
|
|
26
|
+
box.unmountStyles?.();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
22
29
|
}
|
|
23
30
|
};
|
|
24
31
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type SetupImageResult = "loaded" | "error" | "timeout" | "disposed";
|
|
2
|
+
|
|
3
|
+
/** Wait for a terminal image state without blocking setup indefinitely. */
|
|
4
|
+
export const waitForCurrentPageImage = (
|
|
5
|
+
getImage: () => HTMLImageElement | null,
|
|
6
|
+
timeoutMs: number,
|
|
7
|
+
isDisposed: () => boolean,
|
|
8
|
+
): Promise<SetupImageResult> => {
|
|
9
|
+
return new Promise(resolve => {
|
|
10
|
+
let settled = false;
|
|
11
|
+
let pollTimer: number | undefined;
|
|
12
|
+
const settle = (result: SetupImageResult) => {
|
|
13
|
+
if (settled) return;
|
|
14
|
+
settled = true;
|
|
15
|
+
window.clearTimeout(timeoutTimer);
|
|
16
|
+
if (pollTimer !== undefined) window.clearInterval(pollTimer);
|
|
17
|
+
resolve(result);
|
|
18
|
+
};
|
|
19
|
+
const timeoutTimer = window.setTimeout(() => settle("timeout"), timeoutMs);
|
|
20
|
+
const check = () => {
|
|
21
|
+
if (isDisposed()) {
|
|
22
|
+
settle("disposed");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const img = getImage();
|
|
26
|
+
if (img?.complete) {
|
|
27
|
+
if (img.naturalWidth > 0) {
|
|
28
|
+
settle("loaded");
|
|
29
|
+
} else if (img.currentSrc || img.getAttribute("src")?.trim() || img.getAttribute("srcset")?.trim()) {
|
|
30
|
+
// complete is also true before src is assigned. Only an actual
|
|
31
|
+
// image request with zero decoded width represents a failed load.
|
|
32
|
+
settle("error");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
check();
|
|
37
|
+
if (!settled) pollTimer = window.setInterval(check, 100);
|
|
38
|
+
});
|
|
39
|
+
};
|