@netless/app-slide 0.2.106-beta.2 → 0.2.106-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.
- package/dist/index.d.ts +19 -11
- package/dist/main.cjs.js +200 -200
- package/dist/main.cjs.js.map +1 -1
- package/dist/main.es.js +387 -188
- package/dist/main.es.js.map +1 -1
- package/dist/main.iife.js +208 -208
- package/dist/main.iife.js.map +1 -1
- package/package.json +3 -3
- package/src/SlideController/index.ts +224 -40
- package/src/SlideDocsViewer/index.ts +3 -0
- package/src/index.ts +40 -39
- package/src/utils/focus-transition.ts +51 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netless/app-slide",
|
|
3
|
-
"version": "0.2.106-beta.
|
|
3
|
+
"version": "0.2.106-beta.4",
|
|
4
4
|
"main": "dist/main.cjs.js",
|
|
5
5
|
"module": "dist/main.es.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"build": "vite build && npm run types",
|
|
10
10
|
"build:dev": "vite build --mode development && npm run types",
|
|
11
11
|
"cleanup": "rimraf ./dist",
|
|
12
|
-
"test": "rimraf test/.cache && esbuild test/ResizableContainer.test.ts test/NavigationButtonMode.test.ts test/DocsViewerNavigation.integration.test.ts --bundle --platform=node --format=cjs --outdir=test/.cache && node test/.cache/ResizableContainer.test.js && node test/.cache/NavigationButtonMode.test.js && node test/.cache/DocsViewerNavigation.integration.test.js"
|
|
12
|
+
"test": "rimraf test/.cache && esbuild test/ResizableContainer.test.ts test/NavigationButtonMode.test.ts test/DocsViewerNavigation.integration.test.ts test/FocusTransition.test.ts --bundle --platform=node --format=cjs --outdir=test/.cache && node test/.cache/ResizableContainer.test.js && node test/.cache/NavigationButtonMode.test.js && node test/.cache/DocsViewerNavigation.integration.test.js && node test/.cache/FocusTransition.test.js && node test/run-runtime-activity.mjs"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"src",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@juggle/resize-observer": "^3.3.1",
|
|
21
|
-
"@netless/slide": "1.4.
|
|
21
|
+
"@netless/slide": "1.4.62-beta.0",
|
|
22
22
|
"@types/color-string": "^1.5.2",
|
|
23
23
|
"color-string": "^1.9.1",
|
|
24
24
|
"jspdf": "2.5.1",
|
|
@@ -17,8 +17,9 @@ import { SideEffectManager } from "side-effect-manager";
|
|
|
17
17
|
import { Slide, SLIDE_EVENTS } from "@netless/slide";
|
|
18
18
|
import { clamp } from "../utils/helpers";
|
|
19
19
|
import { cachedGetBgColor } from "../utils/bgcolor";
|
|
20
|
-
import { log, verbose, setRoomLogger } from "../utils/logger";
|
|
20
|
+
import { log, verbose, setRoomLogger, logger } from "../utils/logger";
|
|
21
21
|
import { getRoomTracker } from "../utils/tracker";
|
|
22
|
+
import { createFocusTransitionQueue, shouldSlideRuntimeBeActive } from "../utils/focus-transition";
|
|
22
23
|
export { syncSceneWithSlide, createDocsViewerPages } from "./helpers";
|
|
23
24
|
|
|
24
25
|
export const DefaultUrl = "https://convertcdn.netless.link/dynamicConvert";
|
|
@@ -55,6 +56,48 @@ const noop = function noop() {
|
|
|
55
56
|
// do nothing
|
|
56
57
|
};
|
|
57
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
|
+
|
|
58
101
|
type MagixEventListener = Parameters<
|
|
59
102
|
AppContext<Attributes, MagixEvents>["addMagixEventListener"]
|
|
60
103
|
>[1];
|
|
@@ -80,6 +123,7 @@ export class SlideControllerBase {
|
|
|
80
123
|
|
|
81
124
|
protected visible: boolean;
|
|
82
125
|
protected savedIsFrozen: boolean;
|
|
126
|
+
protected focused = true;
|
|
83
127
|
|
|
84
128
|
protected invisibleBehavior: "frozen" | "pause";
|
|
85
129
|
|
|
@@ -132,7 +176,9 @@ export class SlideControllerBase {
|
|
|
132
176
|
log("[Slide] render end", slideIndex);
|
|
133
177
|
} else {
|
|
134
178
|
setTimeout(() => {
|
|
179
|
+
if (this.destroyed) return;
|
|
135
180
|
this.ready = true;
|
|
181
|
+
if (this.isLazySetupMode()) void this.reconcileActivity(true).catch(() => undefined);
|
|
136
182
|
resolve();
|
|
137
183
|
}, 1000);
|
|
138
184
|
}
|
|
@@ -282,6 +328,10 @@ export class SlideControllerBase {
|
|
|
282
328
|
protected pollCount = 0;
|
|
283
329
|
protected pollReadyState = () => {
|
|
284
330
|
if (this.ready) {
|
|
331
|
+
if (this.isLazySetupMode()) {
|
|
332
|
+
void this.reconcileActivity().catch(() => undefined);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
285
335
|
if (this._toFreeze === 1) {
|
|
286
336
|
this.freeze();
|
|
287
337
|
} else if (this._toFreeze === -1) {
|
|
@@ -356,13 +406,30 @@ export class SlideControllerBase {
|
|
|
356
406
|
|
|
357
407
|
protected destroyed = false;
|
|
358
408
|
protected destroyPromise?: Promise<void>;
|
|
409
|
+
private resolveDestroyedSignal!: () => void;
|
|
410
|
+
private readonly destroyedSignal = new Promise<void>(resolve => {
|
|
411
|
+
this.resolveDestroyedSignal = resolve;
|
|
412
|
+
});
|
|
359
413
|
|
|
360
414
|
public destroy(): Promise<void> {
|
|
361
415
|
this.sideEffect.flushAll();
|
|
362
416
|
if (!this.destroyed) {
|
|
363
417
|
log("[Slide] destroy slide (once)");
|
|
364
418
|
this.destroyed = true;
|
|
365
|
-
this.destroyPromise = Promise
|
|
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();
|
|
366
433
|
}
|
|
367
434
|
return this.destroyPromise ?? Promise.resolve();
|
|
368
435
|
}
|
|
@@ -378,49 +445,144 @@ export class SlideControllerBase {
|
|
|
378
445
|
};
|
|
379
446
|
|
|
380
447
|
public isFrozen = false;
|
|
448
|
+
private resourceStateUnknown = false;
|
|
449
|
+
private managedActivity = false;
|
|
381
450
|
protected _toFreeze: -1 | 0 | 1 = 0; // -1: unfreeze, 0: no change, 1: freeze
|
|
382
451
|
|
|
383
|
-
public
|
|
452
|
+
public setFocusedState = (focused: boolean): Promise<void> => {
|
|
453
|
+
this.focused = focused;
|
|
454
|
+
return this.reconcileActivity();
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
private resourceTransition = createFocusTransitionQueue(
|
|
458
|
+
undefined,
|
|
459
|
+
async active => {
|
|
460
|
+
if (this.destroyed) return;
|
|
461
|
+
// A queued release can become stale before the engine starts it.
|
|
462
|
+
if (active && this.shouldBeActive()) await this.unfreeze();
|
|
463
|
+
else await this.freeze();
|
|
464
|
+
},
|
|
465
|
+
error => logger.warn("[Slide] resource transition failed", this.context.appId, error)
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
private shouldBeActive = (): boolean => {
|
|
469
|
+
const wm = this.context.getWindowManager() as any;
|
|
470
|
+
const host = (this.context as any).getRuntimeActivity?.();
|
|
471
|
+
const lazy = this.isLazySetupMode();
|
|
472
|
+
// New hosts own one activity snapshot. Mixing it with the delayed UI
|
|
473
|
+
// boxState can acknowledge "frozen" while the host expects activation.
|
|
474
|
+
// Old hosts retain the original local/Attribute checks.
|
|
475
|
+
const minimized =
|
|
476
|
+
lazy && host
|
|
477
|
+
? !host.active
|
|
478
|
+
: wm?.boxState === "minimized" ||
|
|
479
|
+
wm?.attributes?.minimized === true ||
|
|
480
|
+
this.context.getBoxStatus() === "minimized";
|
|
481
|
+
return (
|
|
482
|
+
shouldSlideRuntimeBeActive(
|
|
483
|
+
!lazy || this.focused,
|
|
484
|
+
document.visibilityState !== "hidden",
|
|
485
|
+
minimized
|
|
486
|
+
) &&
|
|
487
|
+
(!lazy || host?.active !== false)
|
|
488
|
+
);
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
public reconcileActivity = (force = false): Promise<void> => {
|
|
492
|
+
if (this.destroyed) return Promise.resolve();
|
|
493
|
+
if (this.isLazySetupMode()) this.managedActivity = true;
|
|
494
|
+
if (!this.managedActivity) return Promise.resolve();
|
|
495
|
+
this.visible = document.visibilityState !== "hidden";
|
|
496
|
+
return this.resourceTransition(this.shouldBeActive(), force);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
public freeze = async (): Promise<void> => {
|
|
500
|
+
const previousIsFrozen = this.isFrozen;
|
|
501
|
+
const previousToFreeze = this._toFreeze;
|
|
384
502
|
this.isFrozen = true;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
503
|
+
try {
|
|
504
|
+
if (this.ready) {
|
|
505
|
+
log("[Slide] freeze", this.context.appId);
|
|
506
|
+
if (this.invisibleBehavior === "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}`);
|
|
521
|
+
} else {
|
|
522
|
+
this.slide.pause();
|
|
523
|
+
}
|
|
389
524
|
} else {
|
|
390
|
-
this.
|
|
525
|
+
this._toFreeze = 1;
|
|
391
526
|
}
|
|
392
|
-
|
|
393
|
-
|
|
527
|
+
this.resourceStateUnknown = false;
|
|
528
|
+
} catch (error) {
|
|
529
|
+
this.resourceStateUnknown = true;
|
|
530
|
+
this.isFrozen = previousIsFrozen;
|
|
531
|
+
this._toFreeze = previousToFreeze;
|
|
532
|
+
throw error;
|
|
394
533
|
}
|
|
395
|
-
return Promise.resolve();
|
|
396
534
|
};
|
|
397
535
|
|
|
398
536
|
public unfreeze = async (): Promise<void> => {
|
|
399
537
|
if (!this.visible) return;
|
|
400
|
-
if (!this.isFrozen && this.ready) return;
|
|
538
|
+
if (!this.isFrozen && this.ready && !this.resourceStateUnknown) return;
|
|
539
|
+
const previousIsFrozen = this.isFrozen;
|
|
540
|
+
const previousToFreeze = this._toFreeze;
|
|
401
541
|
this.isFrozen = false;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
542
|
+
try {
|
|
543
|
+
if (this.ready) {
|
|
544
|
+
log("[Slide] unfreeze", this.context.appId);
|
|
545
|
+
if (this.invisibleBehavior === "frozen") {
|
|
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}`);
|
|
573
|
+
} else {
|
|
574
|
+
this.slide.resume();
|
|
408
575
|
this.slide.notifyFrameResize();
|
|
409
|
-
});
|
|
410
|
-
isNeedSyncState = true;
|
|
411
|
-
} else {
|
|
412
|
-
this.slide.resume();
|
|
413
|
-
this.slide.notifyFrameResize();
|
|
414
|
-
}
|
|
415
|
-
if (isNeedSyncState) {
|
|
416
|
-
const state = this.context.storage.state.state;
|
|
417
|
-
if (state) {
|
|
418
|
-
log("[Slide] sync storage", JSON.stringify(state));
|
|
419
|
-
this.slide.setSlideState(state);
|
|
420
576
|
}
|
|
577
|
+
} else {
|
|
578
|
+
this._toFreeze = -1;
|
|
421
579
|
}
|
|
422
|
-
|
|
423
|
-
|
|
580
|
+
this.resourceStateUnknown = false;
|
|
581
|
+
} catch (error) {
|
|
582
|
+
this.resourceStateUnknown = true;
|
|
583
|
+
this.isFrozen = previousIsFrozen;
|
|
584
|
+
this._toFreeze = previousToFreeze;
|
|
585
|
+
throw error;
|
|
424
586
|
}
|
|
425
587
|
};
|
|
426
588
|
|
|
@@ -446,6 +608,10 @@ export class SlideControllerBase {
|
|
|
446
608
|
};
|
|
447
609
|
|
|
448
610
|
protected onAppStateChangeHandler = (state: "normal" | "minimized" | "maximized") => {
|
|
611
|
+
if (this.isLazySetupMode() || this.managedActivity) {
|
|
612
|
+
void this.reconcileActivity().catch(() => undefined);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
449
615
|
if (state === "minimized") {
|
|
450
616
|
log("[Slide] freeze because app state is minimized");
|
|
451
617
|
this.freeze();
|
|
@@ -457,6 +623,10 @@ export class SlideControllerBase {
|
|
|
457
623
|
status: "normal" | "minimized" | "maximized";
|
|
458
624
|
}) => {
|
|
459
625
|
const { appId, status } = payload;
|
|
626
|
+
if (appId === this.context.appId && (this.isLazySetupMode() || this.managedActivity)) {
|
|
627
|
+
void this.reconcileActivity().catch(() => undefined);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
460
630
|
if (appId === this.context.appId && status === "minimized") {
|
|
461
631
|
log("[Slide] freeze because app status is minimized");
|
|
462
632
|
this.freeze();
|
|
@@ -479,21 +649,35 @@ export class SlideControllerBase {
|
|
|
479
649
|
}
|
|
480
650
|
};
|
|
481
651
|
|
|
652
|
+
protected isLazySetupMode = (): boolean => {
|
|
653
|
+
try {
|
|
654
|
+
return (this.context.getWindowManager() as any)?.lazySetupInMaximizedMode === true;
|
|
655
|
+
} catch {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
|
|
482
660
|
protected onVisibilityChange = async () => {
|
|
661
|
+
this.visible = document.visibilityState === "visible";
|
|
662
|
+
if (this.isLazySetupMode() || this.managedActivity) {
|
|
663
|
+
await this.reconcileActivity().catch(() => undefined);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
483
666
|
const appStatus = this.getAppStatus();
|
|
667
|
+
if (!this.visible) {
|
|
668
|
+
this.savedIsFrozen = this.isFrozen;
|
|
669
|
+
log("[Slide] freeze because tab becomes invisible");
|
|
670
|
+
if (!this.isFrozen) await this.freeze();
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
484
674
|
if (appStatus === "minimized") {
|
|
485
675
|
log("[Slide] do nothing because app state is minimized");
|
|
486
676
|
return;
|
|
487
677
|
}
|
|
488
|
-
if (!
|
|
489
|
-
this.savedIsFrozen
|
|
490
|
-
|
|
491
|
-
this.freeze();
|
|
492
|
-
} else {
|
|
493
|
-
if (!this.savedIsFrozen) {
|
|
494
|
-
log("[Slide] unfreeze because tab becomes visible", { savedIsFrozen: this.savedIsFrozen });
|
|
495
|
-
this.unfreeze();
|
|
496
|
-
}
|
|
678
|
+
if (!this.savedIsFrozen) {
|
|
679
|
+
log("[Slide] unfreeze because tab becomes visible", { savedIsFrozen: this.savedIsFrozen });
|
|
680
|
+
await this.unfreeze();
|
|
497
681
|
}
|
|
498
682
|
};
|
|
499
683
|
}
|
|
@@ -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";
|
|
@@ -94,8 +95,8 @@ export interface AppOptions
|
|
|
94
95
|
resourceMaxRetries?: number;
|
|
95
96
|
onResourceMaxRetries: (url: string, error: Error) => void;
|
|
96
97
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
98
|
+
* First-render warning threshold in ms. Default: 5_000. New lazy hosts
|
|
99
|
+
* continue awaiting real readiness; legacy/eager hosts resolve on timeout.
|
|
99
100
|
*/
|
|
100
101
|
setupReadyTimeout?: number;
|
|
101
102
|
}
|
|
@@ -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
|
};
|
|
@@ -289,30 +290,19 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> & {
|
|
|
289
290
|
// a lazy-mode-only cache optimization: inactive unless the WindowManager
|
|
290
291
|
// runs with lazySetupInMaximizedMode enabled (read dynamically — lazy can
|
|
291
292
|
// be disabled at runtime, e.g. when forceMaximized is cleared).
|
|
292
|
-
const isBlurFreezeAllowed = (): boolean => {
|
|
293
|
-
const boxStatus = context.getBoxStatus();
|
|
294
|
-
if (boxStatus) return boxStatus !== "normal";
|
|
295
|
-
const boxState = context.getWindowManager()?.boxState;
|
|
296
|
-
return boxState != null && boxState !== "normal";
|
|
297
|
-
};
|
|
298
293
|
sideEffect.add(() =>
|
|
299
|
-
context.emitter.on("focus",
|
|
294
|
+
context.emitter.on("focus", (isFocused: boolean) => {
|
|
300
295
|
if (disposed) return;
|
|
301
|
-
if (!isLazySetupMode()) return;
|
|
302
296
|
const controller = docsViewer?.slideController;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (controller.isFrozen) {
|
|
313
|
-
log("[Slide] focus unfreeze", context.appId);
|
|
314
|
-
await controller.unfreeze();
|
|
315
|
-
}
|
|
297
|
+
// Emittery waits for listener return values. Propagate the transition
|
|
298
|
+
// promise so WindowManager commits focus only after resources are
|
|
299
|
+
// frozen/unfrozen and Slide state restoration has completed.
|
|
300
|
+
return controller?.setFocusedState(isFocused);
|
|
301
|
+
})
|
|
302
|
+
);
|
|
303
|
+
sideEffect.add(() =>
|
|
304
|
+
(context.emitter as any).on("runtimeActivity", () => {
|
|
305
|
+
if (!disposed) return docsViewer?.slideController?.reconcileActivity();
|
|
316
306
|
})
|
|
317
307
|
);
|
|
318
308
|
|
|
@@ -340,17 +330,17 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> & {
|
|
|
340
330
|
};
|
|
341
331
|
offDestroy = context.emitter.on("destroy", teardown);
|
|
342
332
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
(SlideApp as any).__teardownByContext.set(context, teardown);
|
|
333
|
+
const mounting = enqueueSlideWebGLTransition(() => {
|
|
334
|
+
if (!disposed) docsViewer?.mount();
|
|
335
|
+
}, false, `setup:${context.appId}`);
|
|
347
336
|
|
|
348
337
|
// Resolve once the SlideController finished its first render (renderEnd).
|
|
349
|
-
//
|
|
350
|
-
//
|
|
338
|
+
// New lazy hosts retain the real completion after the warning threshold.
|
|
339
|
+
// Legacy/eager hosts retain timed readiness; disposal/failure rejects below.
|
|
351
340
|
const waitForFirstRender = (timeoutMs: number): Promise<boolean> =>
|
|
352
341
|
new Promise<boolean>(resolve => {
|
|
353
342
|
let settled = false;
|
|
343
|
+
let timedOut = false;
|
|
354
344
|
const settle = (loaded: boolean) => {
|
|
355
345
|
if (settled) return;
|
|
356
346
|
settled = true;
|
|
@@ -358,15 +348,23 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> & {
|
|
|
358
348
|
window.clearInterval(pollTimer);
|
|
359
349
|
resolve(loaded);
|
|
360
350
|
};
|
|
361
|
-
const timeoutTimer = window.setTimeout(() =>
|
|
351
|
+
const timeoutTimer = window.setTimeout(() => {
|
|
352
|
+
timedOut = true;
|
|
353
|
+
logger.warn("[Slide] first render still pending", context.appId, timeoutMs);
|
|
354
|
+
if (!(context as any).waitForActualSetupReady) settle(false);
|
|
355
|
+
}, timeoutMs);
|
|
362
356
|
// Success path resolves straight from readyPromise (no polling lag);
|
|
363
357
|
// the poll only catches teardown and pre-ready render failures.
|
|
364
358
|
slideControllerRef?.readyPromise.then(
|
|
365
359
|
() => settle(!firstRenderFailed),
|
|
366
|
-
() =>
|
|
360
|
+
() => {
|
|
361
|
+
firstRenderFailed = true;
|
|
362
|
+
settle(false);
|
|
363
|
+
}
|
|
367
364
|
);
|
|
368
365
|
const pollTimer = window.setInterval(() => {
|
|
369
366
|
if (disposed || firstRenderFailed) settle(false);
|
|
367
|
+
else if (timedOut && !(context as any).waitForActualSetupReady) settle(false);
|
|
370
368
|
}, 100);
|
|
371
369
|
});
|
|
372
370
|
|
|
@@ -507,7 +505,11 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> & {
|
|
|
507
505
|
};
|
|
508
506
|
|
|
509
507
|
const setupReadyTimeout = appOptions?.setupReadyTimeout ?? DEFAULT_SLIDE_SETUP_READY_TIMEOUT;
|
|
510
|
-
return
|
|
508
|
+
return mounting.then(() => {
|
|
509
|
+
if (disposed) throw new Error("[Slide] disposed before first render");
|
|
510
|
+
return waitForFirstRender(setupReadyTimeout);
|
|
511
|
+
}).then(async () => {
|
|
512
|
+
if (disposed) throw new Error("[Slide] disposed before first render");
|
|
511
513
|
if (firstRenderFailed) {
|
|
512
514
|
throw new Error("[Slide] first render failed before ready");
|
|
513
515
|
}
|
|
@@ -515,12 +517,11 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, AppResult> & {
|
|
|
515
517
|
log("[Slide] setup ready wait timed out, slide keeps loading in background");
|
|
516
518
|
}
|
|
517
519
|
return appResult;
|
|
520
|
+
}).catch(async error => {
|
|
521
|
+
if (!disposed) await teardown();
|
|
522
|
+
throw error;
|
|
518
523
|
}) as unknown as AppResult;
|
|
519
524
|
},
|
|
520
|
-
async teardown(context) {
|
|
521
|
-
await (SlideApp as any).__teardownByContext?.get(context)?.();
|
|
522
|
-
(SlideApp as any).__teardownByContext?.delete(context);
|
|
523
|
-
},
|
|
524
525
|
};
|
|
525
526
|
|
|
526
527
|
export default SlideApp;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type FocusTransitionHandler = (isFocused: boolean) => void | Promise<void>;
|
|
2
|
+
|
|
3
|
+
export function shouldSlideRuntimeBeActive(
|
|
4
|
+
isFocused: boolean,
|
|
5
|
+
isVisible: boolean,
|
|
6
|
+
isMinimized: boolean
|
|
7
|
+
): boolean {
|
|
8
|
+
return isFocused && isVisible && !isMinimized;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Drops duplicate focus states and serializes opposite resource transitions.
|
|
13
|
+
* The state tracks accepted events so an in-flight async transition cannot be
|
|
14
|
+
* started twice by duplicate host signals.
|
|
15
|
+
*/
|
|
16
|
+
export function createFocusTransitionQueue(
|
|
17
|
+
initialState: boolean | undefined,
|
|
18
|
+
apply: FocusTransitionHandler,
|
|
19
|
+
onError?: (error: unknown) => void
|
|
20
|
+
): (isFocused: boolean, force?: boolean) => Promise<void> {
|
|
21
|
+
let acceptedState = initialState;
|
|
22
|
+
let revision = 0;
|
|
23
|
+
let queue = Promise.resolve();
|
|
24
|
+
let acceptedPromise = Promise.resolve();
|
|
25
|
+
|
|
26
|
+
return (isFocused, force = false) => {
|
|
27
|
+
// A duplicate signal must observe the same completion barrier. Returning
|
|
28
|
+
// early here would let the host commit focus while freeze/unfreeze is
|
|
29
|
+
// still running.
|
|
30
|
+
if (!force && isFocused === acceptedState) return acceptedPromise;
|
|
31
|
+
acceptedState = isFocused;
|
|
32
|
+
const transitionRevision = ++revision;
|
|
33
|
+
const transition = queue
|
|
34
|
+
.then(() => apply(isFocused))
|
|
35
|
+
.catch(error => {
|
|
36
|
+
// Permit a later same-state signal to retry only when no newer focus
|
|
37
|
+
// transition has superseded the failed request.
|
|
38
|
+
if (revision === transitionRevision) {
|
|
39
|
+
acceptedState = undefined;
|
|
40
|
+
acceptedPromise = Promise.resolve();
|
|
41
|
+
}
|
|
42
|
+
onError?.(error);
|
|
43
|
+
throw error;
|
|
44
|
+
});
|
|
45
|
+
// Keep later opposite transitions runnable after a failure, while the
|
|
46
|
+
// caller still receives the rejection for this exact transition.
|
|
47
|
+
queue = transition.catch(() => undefined);
|
|
48
|
+
acceptedPromise = transition;
|
|
49
|
+
return transition;
|
|
50
|
+
};
|
|
51
|
+
}
|