@netless/app-presentation 0.1.13-beta.3 → 0.1.13
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 +13 -3
- package/dist/index.global.js +101 -44
- package/dist/index.js +101 -44
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +101 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/app-presentation.ts +84 -48
- package/src/presentation.ts +8 -1
- package/src/setup-ready.ts +9 -2
package/src/app-presentation.ts
CHANGED
|
@@ -99,8 +99,8 @@ export interface PresentationAppOptions {
|
|
|
99
99
|
/** useClipView is used to set the presentation use clip view, it will be used in the presentation, and the presentation will be use clip view when the app is initialized */
|
|
100
100
|
useClipView?: boolean;
|
|
101
101
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
102
|
+
* First-image warning threshold in ms. Default: 5_000. New lazy hosts
|
|
103
|
+
* await real readiness; legacy/eager hosts resolve on timeout.
|
|
104
104
|
*/
|
|
105
105
|
setupReadyTimeout?: number;
|
|
106
106
|
/**
|
|
@@ -263,12 +263,34 @@ export const setupBlurThumbnailDegradation = (config: {
|
|
|
263
263
|
const { context, view, pages, pageIndex$, app, dispose } = config;
|
|
264
264
|
const box = context.getBox();
|
|
265
265
|
const boxBlurClass = box.wrapClassName("blur");
|
|
266
|
-
//
|
|
267
|
-
// without a thumbnail don't end it, so a later page degrades again), while
|
|
268
|
-
// `reDegradeEnabled` only turns off when the degraded thumbnail itself
|
|
269
|
-
// fails, so we don't fight the SDK's background onError retry.
|
|
266
|
+
// A failed thumbnail is pinned only for its page during this blur episode.
|
|
270
267
|
let blurEpisode = false;
|
|
271
|
-
let
|
|
268
|
+
let failedThumbnailPageIndex: number | null = null;
|
|
269
|
+
const canReDegrade = (): boolean =>
|
|
270
|
+
blurEpisode && failedThumbnailPageIndex !== pageIndex$.value;
|
|
271
|
+
// Duplicate focus(false) deliveries are unavoidable (multiple sources in
|
|
272
|
+
// the host). Preview-resource clearing is the only non-idempotent DOM op
|
|
273
|
+
// in the blur path, so gate it on this flag until focus restores it.
|
|
274
|
+
let previewSuspended = false;
|
|
275
|
+
// A freshly-created runtime has active resources even if setup completed
|
|
276
|
+
// after the box already lost focus. Track applied resource state instead of
|
|
277
|
+
// reading box.focus, and ignore repeated signals from the host.
|
|
278
|
+
let appliedFocusState = true;
|
|
279
|
+
let logicalFocused = true;
|
|
280
|
+
const activityBlocked = (): boolean => {
|
|
281
|
+
const wm = context.getWindowManager() as any;
|
|
282
|
+
if (wm?.lazySetupInMaximizedMode !== true) return false;
|
|
283
|
+
const host = (context as any).getRuntimeActivity?.();
|
|
284
|
+
// Use the host's complete activity snapshot when available, rather than
|
|
285
|
+
// combining it with a local boxState from a different restore revision.
|
|
286
|
+
if (host) return document.visibilityState === "hidden" || !host.active;
|
|
287
|
+
return (
|
|
288
|
+
document.visibilityState === "hidden" ||
|
|
289
|
+
wm.boxState === "minimized" ||
|
|
290
|
+
wm.attributes?.minimized === true ||
|
|
291
|
+
context.getBoxStatus() === "minimized"
|
|
292
|
+
);
|
|
293
|
+
};
|
|
272
294
|
let boxBlurred = box.$box.classList.contains(boxBlurClass);
|
|
273
295
|
|
|
274
296
|
const setBackgroundSuspended = (suspended: boolean): void => {
|
|
@@ -284,7 +306,7 @@ export const setupBlurThumbnailDegradation = (config: {
|
|
|
284
306
|
const thumbnailURL = page.thumbnail;
|
|
285
307
|
const img = findBackgroundImage(view);
|
|
286
308
|
if (!thumbnailURL || !hasUsableThumbnail(page)) {
|
|
287
|
-
if (boxBlurred) {
|
|
309
|
+
if (boxBlurred || activityBlocked()) {
|
|
288
310
|
setBackgroundSuspended(true);
|
|
289
311
|
if (
|
|
290
312
|
img &&
|
|
@@ -311,10 +333,13 @@ export const setupBlurThumbnailDegradation = (config: {
|
|
|
311
333
|
};
|
|
312
334
|
|
|
313
335
|
const suspendCachedResources = (): void => {
|
|
314
|
-
if (!blurEpisode || !boxBlurred) return;
|
|
315
|
-
|
|
336
|
+
if (!blurEpisode || (!boxBlurred && !activityBlocked())) return;
|
|
337
|
+
if (!previewSuspended) {
|
|
338
|
+
previewSuspended = true;
|
|
339
|
+
app.suspendPreviewResources();
|
|
340
|
+
}
|
|
316
341
|
const page = pages[pageIndex$.value];
|
|
317
|
-
if (!page || !hasUsableThumbnail(page) ||
|
|
342
|
+
if (!page || !hasUsableThumbnail(page) || canReDegrade()) {
|
|
318
343
|
degrade();
|
|
319
344
|
}
|
|
320
345
|
};
|
|
@@ -322,7 +347,8 @@ export const setupBlurThumbnailDegradation = (config: {
|
|
|
322
347
|
const restoreFocusedResources = (): void => {
|
|
323
348
|
if (!blurEpisode) return;
|
|
324
349
|
blurEpisode = false;
|
|
325
|
-
|
|
350
|
+
failedThumbnailPageIndex = null;
|
|
351
|
+
previewSuspended = false;
|
|
326
352
|
restore();
|
|
327
353
|
app.preload.resume(pageIndex$.value);
|
|
328
354
|
};
|
|
@@ -347,39 +373,55 @@ export const setupBlurThumbnailDegradation = (config: {
|
|
|
347
373
|
};
|
|
348
374
|
|
|
349
375
|
const onImgLoad = (ev: Event): void => {
|
|
350
|
-
if (!
|
|
376
|
+
if (!canReDegrade()) return;
|
|
351
377
|
if (!ownsTarget(ev.target)) return;
|
|
352
378
|
degrade();
|
|
353
379
|
};
|
|
354
380
|
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
// still force-restores afterwards.
|
|
381
|
+
// Stop re-degrading this page when its thumbnail fails, so the SDK's
|
|
382
|
+
// onError retry can settle on the full image. Other pages remain eligible.
|
|
358
383
|
const onImgError = (ev: Event): void => {
|
|
359
|
-
if (!
|
|
384
|
+
if (!canReDegrade()) return;
|
|
360
385
|
if (!ownsTarget(ev.target)) return;
|
|
361
|
-
|
|
386
|
+
const thumbnailURL = pages[pageIndex$.value]?.thumbnail;
|
|
387
|
+
if (
|
|
388
|
+
thumbnailURL &&
|
|
389
|
+
resolveImageUrl(ev.target.src) === resolveImageUrl(thumbnailURL)
|
|
390
|
+
) {
|
|
391
|
+
failedThumbnailPageIndex = pageIndex$.value;
|
|
392
|
+
}
|
|
362
393
|
};
|
|
363
394
|
|
|
364
|
-
const
|
|
395
|
+
const applyActivity = (): void => {
|
|
396
|
+
const lazy = (context.getWindowManager() as any)?.lazySetupInMaximizedMode === true;
|
|
397
|
+
const isFocused = (!lazy || logicalFocused) && !activityBlocked();
|
|
398
|
+
if (appliedFocusState === isFocused) return;
|
|
399
|
+
if (!isFocused && !isBlurDegradationAllowed(context) && !activityBlocked()) return;
|
|
365
400
|
if (isFocused) {
|
|
366
401
|
restoreFocusedResources();
|
|
367
402
|
} else {
|
|
368
|
-
if (!isBlurDegradationAllowed(context)) return;
|
|
369
403
|
blurEpisode = true;
|
|
370
|
-
|
|
404
|
+
failedThumbnailPageIndex = null;
|
|
371
405
|
degrade();
|
|
372
406
|
app.preload.pause();
|
|
373
407
|
suspendCachedResources();
|
|
374
408
|
}
|
|
409
|
+
appliedFocusState = isFocused;
|
|
410
|
+
};
|
|
411
|
+
const offFocus = context.emitter.on("focus", (isFocused: boolean) => {
|
|
412
|
+
logicalFocused = isFocused;
|
|
413
|
+
applyActivity();
|
|
375
414
|
});
|
|
376
415
|
dispose.add(offFocus);
|
|
416
|
+
dispose.add((context.emitter as any).on("runtimeActivity", applyActivity));
|
|
417
|
+
document.addEventListener("visibilitychange", applyActivity);
|
|
418
|
+
dispose.add(() => document.removeEventListener("visibilitychange", applyActivity));
|
|
377
419
|
|
|
378
420
|
const updateBoxBlurred = (blurred: boolean): void => {
|
|
379
421
|
if (boxBlurred === blurred) return;
|
|
380
422
|
boxBlurred = blurred;
|
|
381
423
|
if (blurred) suspendCachedResources();
|
|
382
|
-
else restoreFocusedResources();
|
|
424
|
+
else if (logicalFocused && !activityBlocked()) restoreFocusedResources();
|
|
383
425
|
};
|
|
384
426
|
const onBoxBlur = (): void => updateBoxBlurred(true);
|
|
385
427
|
const onBoxFocus = (): void => updateBoxBlurred(false);
|
|
@@ -409,27 +451,16 @@ export const setupBlurThumbnailDegradation = (config: {
|
|
|
409
451
|
});
|
|
410
452
|
};
|
|
411
453
|
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
454
|
+
const getLogger = (context: AppContext) =>
|
|
455
|
+
(context.getWindowManager() as any)?.Logger ?? (context.getRoom() as any)?.logger;
|
|
456
|
+
|
|
457
|
+
const createLogger = (context: AppContext): Logger => {
|
|
458
|
+
return (...args) => getLogger(context)?.info?.(...args);
|
|
418
459
|
};
|
|
419
460
|
|
|
420
|
-
const createDiagnosticLogger = (
|
|
461
|
+
export const createDiagnosticLogger = (
|
|
421
462
|
context: AppContext,
|
|
422
463
|
): PresentationDiagnosticLogger => {
|
|
423
|
-
const createAppLogger = (context as any).createLogger;
|
|
424
|
-
if (typeof createAppLogger === "function") {
|
|
425
|
-
return createAppLogger.call(context, "camera", {
|
|
426
|
-
debounceTime: 300,
|
|
427
|
-
maxWaitTime: 2000,
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Compatibility fallback for WindowManager versions without AppContext.createLogger().
|
|
432
|
-
const roomLogger = (context.getRoom() as any)?.logger;
|
|
433
464
|
const prefix = `[Presentation][${context.appId}][camera]`;
|
|
434
465
|
const emit = (
|
|
435
466
|
level: "info" | "warn" | "error",
|
|
@@ -437,6 +468,7 @@ const createDiagnosticLogger = (
|
|
|
437
468
|
...data: unknown[]
|
|
438
469
|
) => {
|
|
439
470
|
try {
|
|
471
|
+
const roomLogger = getLogger(context);
|
|
440
472
|
const printer = roomLogger?.[level];
|
|
441
473
|
if (typeof printer === "function")
|
|
442
474
|
printer.call(roomLogger, `${prefix}[${event}]`, ...data);
|
|
@@ -516,10 +548,8 @@ export const NetlessAppPresentation: NetlessApp<
|
|
|
516
548
|
|
|
517
549
|
const options = context.getAppOptions() || {};
|
|
518
550
|
const room = context.getRoom();
|
|
519
|
-
const log = options.log || createLogger(
|
|
520
|
-
const
|
|
521
|
-
const warn: Logger = (...data) =>
|
|
522
|
-
roomLogger?.warn ? roomLogger.warn(...data) : log(...data);
|
|
551
|
+
const log = options.log || createLogger(context);
|
|
552
|
+
const warn: Logger = (...data) => getLogger(context)?.warn?.(...data);
|
|
523
553
|
const configuredOriginSize = context.storage.state.originSize;
|
|
524
554
|
const originSize = isValidSize(configuredOriginSize)
|
|
525
555
|
? {
|
|
@@ -889,6 +919,7 @@ export const NetlessAppPresentation: NetlessApp<
|
|
|
889
919
|
view,
|
|
890
920
|
options.thumbnail,
|
|
891
921
|
options.useClipView,
|
|
922
|
+
error => getLogger(context)?.error?.("[Presentation] thumbnail URL parsing failed", context.appId, error),
|
|
892
923
|
),
|
|
893
924
|
);
|
|
894
925
|
app.contentDOM.dataset.appPresentationVersion = __VERSION__;
|
|
@@ -1484,14 +1515,17 @@ export const NetlessAppPresentation: NetlessApp<
|
|
|
1484
1515
|
// `SetupResult | Promise<SetupResult>` declaration is the minimum version.
|
|
1485
1516
|
const setupReadyTimeout = options.setupReadyTimeout ?? DEFAULT_SETUP_READY_TIMEOUT;
|
|
1486
1517
|
return prepareScenesPromise.then(async () => {
|
|
1487
|
-
//
|
|
1488
|
-
//
|
|
1518
|
+
// Wait for the current page image or its first failure. The SDK owns
|
|
1519
|
+
// background-image retries, so a failed request must not fail setup.
|
|
1489
1520
|
const result = await waitForCurrentPageImage(
|
|
1490
1521
|
() => findBackgroundImage(view), setupReadyTimeout, () => disposed,
|
|
1522
|
+
{
|
|
1523
|
+
waitForReady: () => Boolean((context as any).waitForActualSetupReady),
|
|
1524
|
+
onTimeout: () => warn(`[Presentation] setup page image still pending after ${setupReadyTimeout}ms`),
|
|
1525
|
+
},
|
|
1491
1526
|
);
|
|
1492
|
-
if (
|
|
1493
|
-
|
|
1494
|
-
} else if (result === "error" && !disposed) {
|
|
1527
|
+
if (disposed) throw new Error("[Presentation] disposed before setup ready");
|
|
1528
|
+
if (result === "error") {
|
|
1495
1529
|
warn("[Presentation] setup page image failed to load; continuing setup");
|
|
1496
1530
|
}
|
|
1497
1531
|
return controller;
|
|
@@ -1553,6 +1587,7 @@ function createPresentation(
|
|
|
1553
1587
|
view: View,
|
|
1554
1588
|
thumbnail?: (src: string) => string,
|
|
1555
1589
|
useClipView?: boolean,
|
|
1590
|
+
onThumbnailError?: (error: unknown) => void,
|
|
1556
1591
|
): AppPresentation {
|
|
1557
1592
|
box.mountStyles(styles);
|
|
1558
1593
|
|
|
@@ -1561,6 +1596,7 @@ function createPresentation(
|
|
|
1561
1596
|
readonly: box.readonly,
|
|
1562
1597
|
jumpPage,
|
|
1563
1598
|
thumbnail,
|
|
1599
|
+
onThumbnailError,
|
|
1564
1600
|
});
|
|
1565
1601
|
app.box = box;
|
|
1566
1602
|
box.mountContent(app.contentDOM);
|
package/src/presentation.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface PresentationConfig {
|
|
|
18
18
|
readonly pages: PresentationPage[]
|
|
19
19
|
readonly readonly?: boolean
|
|
20
20
|
readonly thumbnail?: (src: string) => string
|
|
21
|
+
readonly onThumbnailError?: (error: unknown) => void
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -59,6 +60,7 @@ export class Presentation implements IDisposable<void> {
|
|
|
59
60
|
readonly dispose = disposableStore()
|
|
60
61
|
readonly pages: PresentationPage[]
|
|
61
62
|
readonly preload: Preload
|
|
63
|
+
readonly onThumbnailError?: (error: unknown) => void
|
|
62
64
|
|
|
63
65
|
dom: Element | DocumentFragment
|
|
64
66
|
contentDOM: HTMLDivElement
|
|
@@ -81,6 +83,7 @@ export class Presentation implements IDisposable<void> {
|
|
|
81
83
|
constructor(config: PresentationConfig) {
|
|
82
84
|
this.pages = config.pages
|
|
83
85
|
this.preload = new Preload(this.pages)
|
|
86
|
+
this.onThumbnailError = config.onThumbnailError
|
|
84
87
|
if (config.thumbnail) this.thumbnail = config.thumbnail
|
|
85
88
|
this.dispose.add(this.preload)
|
|
86
89
|
this.readonly = config.readonly ?? false
|
|
@@ -329,7 +332,11 @@ export class Presentation implements IDisposable<void> {
|
|
|
329
332
|
url.searchParams.set('x-oss-process', 'image/resize,l_50')
|
|
330
333
|
return url.toString()
|
|
331
334
|
} catch (err) {
|
|
332
|
-
|
|
335
|
+
try {
|
|
336
|
+
this.onThumbnailError?.(err)
|
|
337
|
+
} catch {
|
|
338
|
+
// A diagnostic failure must not change the thumbnail fallback.
|
|
339
|
+
}
|
|
333
340
|
return src
|
|
334
341
|
}
|
|
335
342
|
}
|
package/src/setup-ready.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
export type SetupImageResult = "loaded" | "error" | "timeout" | "disposed";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** New lazy hosts await actual readiness; legacy hosts retain the bounded wait. */
|
|
4
4
|
export const waitForCurrentPageImage = (
|
|
5
5
|
getImage: () => HTMLImageElement | null,
|
|
6
6
|
timeoutMs: number,
|
|
7
7
|
isDisposed: () => boolean,
|
|
8
|
+
options?: { waitForReady: () => boolean; onTimeout: () => void },
|
|
8
9
|
): Promise<SetupImageResult> => {
|
|
9
10
|
return new Promise(resolve => {
|
|
10
11
|
let settled = false;
|
|
12
|
+
let timedOut = false;
|
|
11
13
|
let pollTimer: number | undefined;
|
|
12
14
|
const settle = (result: SetupImageResult) => {
|
|
13
15
|
if (settled) return;
|
|
@@ -16,12 +18,17 @@ export const waitForCurrentPageImage = (
|
|
|
16
18
|
if (pollTimer !== undefined) window.clearInterval(pollTimer);
|
|
17
19
|
resolve(result);
|
|
18
20
|
};
|
|
19
|
-
const timeoutTimer = window.setTimeout(() =>
|
|
21
|
+
const timeoutTimer = window.setTimeout(() => {
|
|
22
|
+
timedOut = true;
|
|
23
|
+
options?.onTimeout();
|
|
24
|
+
if (!options?.waitForReady()) settle("timeout");
|
|
25
|
+
}, timeoutMs);
|
|
20
26
|
const check = () => {
|
|
21
27
|
if (isDisposed()) {
|
|
22
28
|
settle("disposed");
|
|
23
29
|
return;
|
|
24
30
|
}
|
|
31
|
+
if (timedOut && !options?.waitForReady()) { settle("timeout"); return; }
|
|
25
32
|
const img = getImage();
|
|
26
33
|
if (img?.complete) {
|
|
27
34
|
if (img.naturalWidth > 0) {
|