@netless/app-presentation 0.1.13-beta.3 → 0.1.13-beta.4

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.
@@ -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
- * Max time (ms) `setup()` waits for the first visible page image to load
103
- * before resolving anyway. Default: 5_000.
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
  /**
@@ -269,6 +269,29 @@ export const setupBlurThumbnailDegradation = (config: {
269
269
  // fails, so we don't fight the SDK's background onError retry.
270
270
  let blurEpisode = false;
271
271
  let reDegradeEnabled = false;
272
+ // Duplicate focus(false) deliveries are unavoidable (multiple sources in
273
+ // the host). Preview-resource clearing is the only non-idempotent DOM op
274
+ // in the blur path, so gate it on this flag until focus restores it.
275
+ let previewSuspended = false;
276
+ // A freshly-created runtime has active resources even if setup completed
277
+ // after the box already lost focus. Track applied resource state instead of
278
+ // reading box.focus, and ignore repeated signals from the host.
279
+ let appliedFocusState = true;
280
+ let logicalFocused = true;
281
+ const activityBlocked = (): boolean => {
282
+ const wm = context.getWindowManager() as any;
283
+ if (wm?.lazySetupInMaximizedMode !== true) return false;
284
+ const host = (context as any).getRuntimeActivity?.();
285
+ // Use the host's complete activity snapshot when available, rather than
286
+ // combining it with a local boxState from a different restore revision.
287
+ if (host) return document.visibilityState === "hidden" || !host.active;
288
+ return (
289
+ document.visibilityState === "hidden" ||
290
+ wm.boxState === "minimized" ||
291
+ wm.attributes?.minimized === true ||
292
+ context.getBoxStatus() === "minimized"
293
+ );
294
+ };
272
295
  let boxBlurred = box.$box.classList.contains(boxBlurClass);
273
296
 
274
297
  const setBackgroundSuspended = (suspended: boolean): void => {
@@ -284,7 +307,7 @@ export const setupBlurThumbnailDegradation = (config: {
284
307
  const thumbnailURL = page.thumbnail;
285
308
  const img = findBackgroundImage(view);
286
309
  if (!thumbnailURL || !hasUsableThumbnail(page)) {
287
- if (boxBlurred) {
310
+ if (boxBlurred || activityBlocked()) {
288
311
  setBackgroundSuspended(true);
289
312
  if (
290
313
  img &&
@@ -311,8 +334,11 @@ export const setupBlurThumbnailDegradation = (config: {
311
334
  };
312
335
 
313
336
  const suspendCachedResources = (): void => {
314
- if (!blurEpisode || !boxBlurred) return;
315
- app.suspendPreviewResources();
337
+ if (!blurEpisode || (!boxBlurred && !activityBlocked())) return;
338
+ if (!previewSuspended) {
339
+ previewSuspended = true;
340
+ app.suspendPreviewResources();
341
+ }
316
342
  const page = pages[pageIndex$.value];
317
343
  if (!page || !hasUsableThumbnail(page) || reDegradeEnabled) {
318
344
  degrade();
@@ -323,6 +349,7 @@ export const setupBlurThumbnailDegradation = (config: {
323
349
  if (!blurEpisode) return;
324
350
  blurEpisode = false;
325
351
  reDegradeEnabled = false;
352
+ previewSuspended = false;
326
353
  restore();
327
354
  app.preload.resume(pageIndex$.value);
328
355
  };
@@ -361,25 +388,36 @@ export const setupBlurThumbnailDegradation = (config: {
361
388
  reDegradeEnabled = false;
362
389
  };
363
390
 
364
- const offFocus = context.emitter.on("focus", (isFocused: boolean) => {
391
+ const applyActivity = (): void => {
392
+ const lazy = (context.getWindowManager() as any)?.lazySetupInMaximizedMode === true;
393
+ const isFocused = (!lazy || logicalFocused) && !activityBlocked();
394
+ if (appliedFocusState === isFocused) return;
395
+ if (!isFocused && !isBlurDegradationAllowed(context) && !activityBlocked()) return;
365
396
  if (isFocused) {
366
397
  restoreFocusedResources();
367
398
  } else {
368
- if (!isBlurDegradationAllowed(context)) return;
369
399
  blurEpisode = true;
370
400
  reDegradeEnabled = true;
371
401
  degrade();
372
402
  app.preload.pause();
373
403
  suspendCachedResources();
374
404
  }
405
+ appliedFocusState = isFocused;
406
+ };
407
+ const offFocus = context.emitter.on("focus", (isFocused: boolean) => {
408
+ logicalFocused = isFocused;
409
+ applyActivity();
375
410
  });
376
411
  dispose.add(offFocus);
412
+ dispose.add((context.emitter as any).on("runtimeActivity", applyActivity));
413
+ document.addEventListener("visibilitychange", applyActivity);
414
+ dispose.add(() => document.removeEventListener("visibilitychange", applyActivity));
377
415
 
378
416
  const updateBoxBlurred = (blurred: boolean): void => {
379
417
  if (boxBlurred === blurred) return;
380
418
  boxBlurred = blurred;
381
419
  if (blurred) suspendCachedResources();
382
- else restoreFocusedResources();
420
+ else if (logicalFocused && !activityBlocked()) restoreFocusedResources();
383
421
  };
384
422
  const onBoxBlur = (): void => updateBoxBlurred(true);
385
423
  const onBoxFocus = (): void => updateBoxBlurred(false);
@@ -1488,10 +1526,16 @@ export const NetlessAppPresentation: NetlessApp<
1488
1526
  // page image is actually visible (or the bounded wait gives up).
1489
1527
  const result = await waitForCurrentPageImage(
1490
1528
  () => findBackgroundImage(view), setupReadyTimeout, () => disposed,
1529
+ {
1530
+ waitForReady: () => Boolean((context as any).waitForActualSetupReady),
1531
+ onTimeout: () => warn(`[Presentation] setup page image still pending after ${setupReadyTimeout}ms`),
1532
+ },
1491
1533
  );
1492
- if (result === "timeout" && !disposed) {
1493
- warn(`[Presentation] setup ready wait timed out after ${setupReadyTimeout}ms`);
1494
- } else if (result === "error" && !disposed) {
1534
+ if (disposed) throw new Error("[Presentation] disposed before setup ready");
1535
+ if (result === "error" && (context as any).waitForActualSetupReady) {
1536
+ throw new Error("[Presentation] setup page image failed to load");
1537
+ }
1538
+ if (result === "error" && !disposed) {
1495
1539
  warn("[Presentation] setup page image failed to load; continuing setup");
1496
1540
  }
1497
1541
  return controller;
@@ -1,13 +1,15 @@
1
1
  export type SetupImageResult = "loaded" | "error" | "timeout" | "disposed";
2
2
 
3
- /** Wait for a terminal image state without blocking setup indefinitely. */
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(() => settle("timeout"), timeoutMs);
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) {