@netless/app-presentation 0.1.13-beta.4 → 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.
@@ -263,12 +263,11 @@ 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
- // Two separate flags: `blurEpisode` covers the whole blurred period (pages
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 reDegradeEnabled = false;
268
+ let failedThumbnailPageIndex: number | null = null;
269
+ const canReDegrade = (): boolean =>
270
+ blurEpisode && failedThumbnailPageIndex !== pageIndex$.value;
272
271
  // Duplicate focus(false) deliveries are unavoidable (multiple sources in
273
272
  // the host). Preview-resource clearing is the only non-idempotent DOM op
274
273
  // in the blur path, so gate it on this flag until focus restores it.
@@ -340,7 +339,7 @@ export const setupBlurThumbnailDegradation = (config: {
340
339
  app.suspendPreviewResources();
341
340
  }
342
341
  const page = pages[pageIndex$.value];
343
- if (!page || !hasUsableThumbnail(page) || reDegradeEnabled) {
342
+ if (!page || !hasUsableThumbnail(page) || canReDegrade()) {
344
343
  degrade();
345
344
  }
346
345
  };
@@ -348,7 +347,7 @@ export const setupBlurThumbnailDegradation = (config: {
348
347
  const restoreFocusedResources = (): void => {
349
348
  if (!blurEpisode) return;
350
349
  blurEpisode = false;
351
- reDegradeEnabled = false;
350
+ failedThumbnailPageIndex = null;
352
351
  previewSuspended = false;
353
352
  restore();
354
353
  app.preload.resume(pageIndex$.value);
@@ -374,18 +373,23 @@ export const setupBlurThumbnailDegradation = (config: {
374
373
  };
375
374
 
376
375
  const onImgLoad = (ev: Event): void => {
377
- if (!blurEpisode || !reDegradeEnabled) return;
376
+ if (!canReDegrade()) return;
378
377
  if (!ownsTarget(ev.target)) return;
379
378
  degrade();
380
379
  };
381
380
 
382
- // The degraded thumbnail failed to load: stop re-degrading for this blur
383
- // episode so the SDK's onError retry can settle on the full image. Focus
384
- // 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.
385
383
  const onImgError = (ev: Event): void => {
386
- if (!blurEpisode || !reDegradeEnabled) return;
384
+ if (!canReDegrade()) return;
387
385
  if (!ownsTarget(ev.target)) return;
388
- reDegradeEnabled = false;
386
+ const thumbnailURL = pages[pageIndex$.value]?.thumbnail;
387
+ if (
388
+ thumbnailURL &&
389
+ resolveImageUrl(ev.target.src) === resolveImageUrl(thumbnailURL)
390
+ ) {
391
+ failedThumbnailPageIndex = pageIndex$.value;
392
+ }
389
393
  };
390
394
 
391
395
  const applyActivity = (): void => {
@@ -397,7 +401,7 @@ export const setupBlurThumbnailDegradation = (config: {
397
401
  restoreFocusedResources();
398
402
  } else {
399
403
  blurEpisode = true;
400
- reDegradeEnabled = true;
404
+ failedThumbnailPageIndex = null;
401
405
  degrade();
402
406
  app.preload.pause();
403
407
  suspendCachedResources();
@@ -447,27 +451,16 @@ export const setupBlurThumbnailDegradation = (config: {
447
451
  });
448
452
  };
449
453
 
450
- const createLogger = (room: Room | undefined): Logger => {
451
- if (room && (room as any).logger) {
452
- return (...args) => (room as any).logger.info(...args);
453
- } else {
454
- return (...args) => console.log(...args);
455
- }
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);
456
459
  };
457
460
 
458
- const createDiagnosticLogger = (
461
+ export const createDiagnosticLogger = (
459
462
  context: AppContext,
460
463
  ): PresentationDiagnosticLogger => {
461
- const createAppLogger = (context as any).createLogger;
462
- if (typeof createAppLogger === "function") {
463
- return createAppLogger.call(context, "camera", {
464
- debounceTime: 300,
465
- maxWaitTime: 2000,
466
- });
467
- }
468
-
469
- // Compatibility fallback for WindowManager versions without AppContext.createLogger().
470
- const roomLogger = (context.getRoom() as any)?.logger;
471
464
  const prefix = `[Presentation][${context.appId}][camera]`;
472
465
  const emit = (
473
466
  level: "info" | "warn" | "error",
@@ -475,6 +468,7 @@ const createDiagnosticLogger = (
475
468
  ...data: unknown[]
476
469
  ) => {
477
470
  try {
471
+ const roomLogger = getLogger(context);
478
472
  const printer = roomLogger?.[level];
479
473
  if (typeof printer === "function")
480
474
  printer.call(roomLogger, `${prefix}[${event}]`, ...data);
@@ -554,10 +548,8 @@ export const NetlessAppPresentation: NetlessApp<
554
548
 
555
549
  const options = context.getAppOptions() || {};
556
550
  const room = context.getRoom();
557
- const log = options.log || createLogger(room);
558
- const roomLogger = (room as any)?.logger;
559
- const warn: Logger = (...data) =>
560
- roomLogger?.warn ? roomLogger.warn(...data) : log(...data);
551
+ const log = options.log || createLogger(context);
552
+ const warn: Logger = (...data) => getLogger(context)?.warn?.(...data);
561
553
  const configuredOriginSize = context.storage.state.originSize;
562
554
  const originSize = isValidSize(configuredOriginSize)
563
555
  ? {
@@ -927,6 +919,7 @@ export const NetlessAppPresentation: NetlessApp<
927
919
  view,
928
920
  options.thumbnail,
929
921
  options.useClipView,
922
+ error => getLogger(context)?.error?.("[Presentation] thumbnail URL parsing failed", context.appId, error),
930
923
  ),
931
924
  );
932
925
  app.contentDOM.dataset.appPresentationVersion = __VERSION__;
@@ -1522,8 +1515,8 @@ export const NetlessAppPresentation: NetlessApp<
1522
1515
  // `SetupResult | Promise<SetupResult>` declaration is the minimum version.
1523
1516
  const setupReadyTimeout = options.setupReadyTimeout ?? DEFAULT_SETUP_READY_TIMEOUT;
1524
1517
  return prepareScenesPromise.then(async () => {
1525
- // Keep the serial setup queue honest: resolve only once the current
1526
- // page image is actually visible (or the bounded wait gives up).
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.
1527
1520
  const result = await waitForCurrentPageImage(
1528
1521
  () => findBackgroundImage(view), setupReadyTimeout, () => disposed,
1529
1522
  {
@@ -1532,10 +1525,7 @@ export const NetlessAppPresentation: NetlessApp<
1532
1525
  },
1533
1526
  );
1534
1527
  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) {
1528
+ if (result === "error") {
1539
1529
  warn("[Presentation] setup page image failed to load; continuing setup");
1540
1530
  }
1541
1531
  return controller;
@@ -1597,6 +1587,7 @@ function createPresentation(
1597
1587
  view: View,
1598
1588
  thumbnail?: (src: string) => string,
1599
1589
  useClipView?: boolean,
1590
+ onThumbnailError?: (error: unknown) => void,
1600
1591
  ): AppPresentation {
1601
1592
  box.mountStyles(styles);
1602
1593
 
@@ -1605,6 +1596,7 @@ function createPresentation(
1605
1596
  readonly: box.readonly,
1606
1597
  jumpPage,
1607
1598
  thumbnail,
1599
+ onThumbnailError,
1608
1600
  });
1609
1601
  app.box = box;
1610
1602
  box.mountContent(app.contentDOM);
@@ -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
- console.error(err)
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
  }