@netless/app-slide 0.2.106-beta.3 → 0.2.106-beta.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netless/app-slide",
3
- "version": "0.2.106-beta.3",
3
+ "version": "0.2.106-beta.5",
4
4
  "main": "dist/main.cjs.js",
5
5
  "module": "dist/main.es.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "devDependencies": {
20
20
  "@juggle/resize-observer": "^3.3.1",
21
- "@netless/slide": "1.4.61",
21
+ "@netless/slide": "1.4.62-beta.1",
22
22
  "@types/color-string": "^1.5.2",
23
23
  "color-string": "^1.9.1",
24
24
  "jspdf": "2.5.1",
@@ -56,6 +56,48 @@ const noop = function noop() {
56
56
  // do nothing
57
57
  };
58
58
 
59
+ type SlideWebGLTask = {
60
+ label: string;
61
+ run: () => void | Promise<void>;
62
+ resolve: () => void;
63
+ reject: (error: unknown) => void;
64
+ };
65
+
66
+ const slideWebGLTasks: SlideWebGLTask[] = [];
67
+ let slideWebGLRunning = false;
68
+
69
+ function runNextSlideWebGLTask(): void {
70
+ if (slideWebGLRunning) return;
71
+ const task = slideWebGLTasks.shift();
72
+ if (!task) return;
73
+ slideWebGLRunning = true;
74
+ const startedAt = performance.now();
75
+ log("[Slide][player-queue] start", task.label, "pending", slideWebGLTasks.length);
76
+ void Promise.resolve().then(task.run).then(() => {
77
+ log("[Slide][player-queue] done", task.label, "ms", Math.round(performance.now() - startedAt));
78
+ task.resolve();
79
+ }, error => {
80
+ log("[Slide][player-queue] failed", task.label, error);
81
+ task.reject(error);
82
+ }).finally(() => {
83
+ slideWebGLRunning = false;
84
+ runNextSlideWebGLTask();
85
+ });
86
+ }
87
+
88
+ export function enqueueSlideWebGLTransition(
89
+ run: () => void | Promise<void>,
90
+ priority = false,
91
+ label = "unspecified"
92
+ ): Promise<void> {
93
+ return new Promise<void>((resolve, reject) => {
94
+ const task = { label, run, resolve, reject };
95
+ if (priority) slideWebGLTasks.unshift(task);
96
+ else slideWebGLTasks.push(task);
97
+ runNextSlideWebGLTask();
98
+ });
99
+ }
100
+
59
101
  type MagixEventListener = Parameters<
60
102
  AppContext<Attributes, MagixEvents>["addMagixEventListener"]
61
103
  >[1];
@@ -364,13 +406,30 @@ export class SlideControllerBase {
364
406
 
365
407
  protected destroyed = false;
366
408
  protected destroyPromise?: Promise<void>;
409
+ private resolveDestroyedSignal!: () => void;
410
+ private readonly destroyedSignal = new Promise<void>(resolve => {
411
+ this.resolveDestroyedSignal = resolve;
412
+ });
367
413
 
368
414
  public destroy(): Promise<void> {
369
415
  this.sideEffect.flushAll();
370
416
  if (!this.destroyed) {
371
417
  log("[Slide] destroy slide (once)");
372
418
  this.destroyed = true;
373
- this.destroyPromise = Promise.resolve(this.slide.destroy());
419
+ this.destroyPromise = enqueueSlideWebGLTransition(() => new Promise<void>((resolve, reject) => {
420
+ try {
421
+ (this.slide.destroy as (
422
+ onPlayerDestroyed?: () => void,
423
+ onError?: (error: unknown) => void
424
+ ) => void).call(this.slide, () => {
425
+ log("[Slide][player] destroy callback", this.context.appId);
426
+ resolve();
427
+ }, reject);
428
+ } catch (error) {
429
+ reject(error);
430
+ }
431
+ }), true, `destroy:${this.context.appId}`);
432
+ this.resolveDestroyedSignal();
374
433
  }
375
434
  return this.destroyPromise ?? Promise.resolve();
376
435
  }
@@ -445,7 +504,20 @@ export class SlideControllerBase {
445
504
  if (this.ready) {
446
505
  log("[Slide] freeze", this.context.appId);
447
506
  if (this.invisibleBehavior === "frozen") {
448
- await this.slide.frozen();
507
+ await enqueueSlideWebGLTransition(() => {
508
+ if (this.destroyed) return Promise.resolve();
509
+ const destroyed = new Promise<void>((resolve, reject) => {
510
+ const result = (this.slide.frozen as (
511
+ onPlayerDestroyed?: () => void,
512
+ onError?: (error: unknown) => void
513
+ ) => void | Promise<void>).call(this.slide, () => {
514
+ log("[Slide][player] frozen callback", this.context.appId);
515
+ resolve();
516
+ }, reject);
517
+ if (result) void result.then(resolve, reject);
518
+ });
519
+ return Promise.race([destroyed, this.destroyedSignal]);
520
+ }, false, `freeze:${this.context.appId}`);
449
521
  } else {
450
522
  this.slide.pause();
451
523
  }
@@ -469,25 +541,39 @@ export class SlideControllerBase {
469
541
  this.isFrozen = false;
470
542
  try {
471
543
  if (this.ready) {
472
- let isNeedSyncState = false;
473
544
  log("[Slide] unfreeze", this.context.appId);
474
545
  if (this.invisibleBehavior === "frozen") {
475
- await this.slide.release(() => {
476
- // release 会重建 player,内部 observer 已关闭,需要按当前 frame 尺寸重绘
477
- if (!this.isLazySetupMode() || this.shouldBeActive()) this.slide.notifyFrameResize();
478
- });
479
- isNeedSyncState = true;
546
+ await enqueueSlideWebGLTransition(() => {
547
+ if (this.destroyed) return Promise.resolve();
548
+ const created = new Promise<void>((resolve, reject) => {
549
+ const result = (this.slide.release as (
550
+ onRestored?: () => void,
551
+ onPlayerCreated?: () => void,
552
+ onError?: (error: unknown) => void
553
+ ) => void | Promise<void>).call(this.slide, () => {
554
+ log("[Slide][player] restored callback", this.context.appId);
555
+ if (this.isLazySetupMode() && !this.shouldBeActive()) return;
556
+ this.slide.notifyFrameResize();
557
+ const state = this.context.storage.state.state;
558
+ if (state) {
559
+ log("[Slide] sync storage", JSON.stringify(state));
560
+ void this.slide.setSlideState(state).catch(error => {
561
+ this.resourceStateUnknown = true;
562
+ logger.warn("[Slide] storage sync after release failed", this.context.appId, error);
563
+ });
564
+ }
565
+ }, () => {
566
+ log("[Slide][player] created callback", this.context.appId);
567
+ resolve();
568
+ }, reject);
569
+ if (result) void result.then(resolve, reject);
570
+ });
571
+ return Promise.race([created, this.destroyedSignal]);
572
+ }, false, `release:${this.context.appId}`);
480
573
  } else {
481
574
  this.slide.resume();
482
575
  this.slide.notifyFrameResize();
483
576
  }
484
- if (isNeedSyncState && (!this.isLazySetupMode() || this.shouldBeActive())) {
485
- const state = this.context.storage.state.state;
486
- if (state) {
487
- log("[Slide] sync storage", JSON.stringify(state));
488
- await this.slide.setSlideState(state);
489
- }
490
- }
491
577
  } else {
492
578
  this._toFreeze = -1;
493
579
  }
@@ -435,6 +435,7 @@ export class SlideDocsViewer {
435
435
  };
436
436
 
437
437
  protected onRenderStart = () => {
438
+ logger.log("[Slide][notes] hide", this.appId, "scene", this.whiteboardView.focusScenePath);
438
439
  this.$whiteboardView.classList.add(this.wrapClassName("wb-view-hidden"));
439
440
  this.viewer.setPlaying();
440
441
  };
@@ -447,11 +448,13 @@ export class SlideDocsViewer {
447
448
 
448
449
  public onPageChanged = () => {
449
450
  clearTimeout(this._onPageChangedTimer);
451
+ logger.log("[Slide][notes] show scheduled", this.appId, "scene", this.whiteboardView.focusScenePath);
450
452
  this._onPageChangedTimer = setTimeout(this._onPageChanged, 200) as unknown as number;
451
453
  };
452
454
 
453
455
  protected _onPageChangedTimer = 0;
454
456
  protected _onPageChanged = () => {
457
+ logger.log("[Slide][notes] show", this.appId, "scene", this.whiteboardView.focusScenePath);
455
458
  this.$overlay.style.opacity = "";
456
459
  this.$whiteboardView.classList.remove(this.wrapClassName("wb-view-hidden"));
457
460
  };
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  syncSceneWithSlide,
14
14
  SlideController,
15
15
  SlideControllerBase,
16
+ enqueueSlideWebGLTransition,
16
17
  } from "./SlideController";
17
18
  import { SlideDocsViewer } from "./SlideDocsViewer";
18
19
  import { apps, FreezerLength, addHooks, useFreezer } from "./utils/freezer";
@@ -126,9 +127,7 @@ export interface AppResult {
126
127
  playPptMedia: () => void;
127
128
  }
128
129
 
129
- const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> & {
130
- teardown(context: import("@netless/window-manager").AppContext): Promise<void>;
131
- } = {
130
+ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> = {
132
131
  kind: "Slide",
133
132
  setup(context) {
134
133
  console.log("[Slide] setup @ " + version);
@@ -225,6 +224,8 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> & {
225
224
  const page = slideController.slide.slideState.currentSlideIndex;
226
225
  log("[Slide] page to", page, synced ? "(synced)" : "", "(on ready)");
227
226
  slideController.slide.on("renderEnd", options.onRenderEnd);
227
+ }).catch(error => {
228
+ if (!disposed) logger.warn("[Slide] ready callback failed", context.appId, error);
228
229
  });
229
230
  return slideController;
230
231
  };
@@ -329,10 +330,9 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> & {
329
330
  };
330
331
  offDestroy = context.emitter.on("destroy", teardown);
331
332
 
332
- docsViewer.mount();
333
-
334
- (SlideApp as any).__teardownByContext ||= new WeakMap<object, () => Promise<void>>();
335
- (SlideApp as any).__teardownByContext.set(context, teardown);
333
+ const mounting = enqueueSlideWebGLTransition(() => {
334
+ if (!disposed) docsViewer?.mount();
335
+ }, false, `setup:${context.appId}`);
336
336
 
337
337
  // Resolve once the SlideController finished its first render (renderEnd).
338
338
  // New lazy hosts retain the real completion after the warning threshold.
@@ -357,7 +357,10 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> & {
357
357
  // the poll only catches teardown and pre-ready render failures.
358
358
  slideControllerRef?.readyPromise.then(
359
359
  () => settle(!firstRenderFailed),
360
- () => settle(false)
360
+ () => {
361
+ firstRenderFailed = true;
362
+ settle(false);
363
+ }
361
364
  );
362
365
  const pollTimer = window.setInterval(() => {
363
366
  if (disposed || firstRenderFailed) settle(false);
@@ -502,7 +505,10 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> & {
502
505
  };
503
506
 
504
507
  const setupReadyTimeout = appOptions?.setupReadyTimeout ?? DEFAULT_SLIDE_SETUP_READY_TIMEOUT;
505
- return waitForFirstRender(setupReadyTimeout).then(() => {
508
+ return mounting.then(() => {
509
+ if (disposed) throw new Error("[Slide] disposed before first render");
510
+ return waitForFirstRender(setupReadyTimeout);
511
+ }).then(async () => {
506
512
  if (disposed) throw new Error("[Slide] disposed before first render");
507
513
  if (firstRenderFailed) {
508
514
  throw new Error("[Slide] first render failed before ready");
@@ -511,12 +517,11 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> & {
511
517
  log("[Slide] setup ready wait timed out, slide keeps loading in background");
512
518
  }
513
519
  return appResult;
520
+ }).catch(async error => {
521
+ if (!disposed) await teardown();
522
+ throw error;
514
523
  }) as unknown as AppResult;
515
524
  },
516
- async teardown(context) {
517
- await (SlideApp as any).__teardownByContext?.get(context)?.();
518
- (SlideApp as any).__teardownByContext?.delete(context);
519
- },
520
525
  };
521
526
 
522
527
  export default SlideApp;