mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
refactor(editor): rx state management in turbo renderer (#11200)
# Refactor Turbo Renderer State Management to Use RxJS ### TL;DR Refactored the TurboRenderer state management to use RxJS observables instead of direct state mutations, improving state transitions and reactivity. ### What changed? - Replaced the public `state` property with a private `state$` BehaviorSubject in `ViewportTurboRendererExtension` - Added proper state transition logging using RxJS operators - Combined multiple event subscriptions using `merge` operator for better organization - Improved state transition logic in the `refresh()` method - Updated the `zooming$` and `panning$` signals in the Viewport ### Why make this change? This refactoring improves the codebase by: 1. Using a more consistent reactive programming model with RxJS 2. Making state transitions more explicit and traceable 3. Reducing potential bugs from manual state management 4. Improving code organization by combining related event streams 5. Ensuring proper cleanup of resources when components are disposed The change maintains the same functionality while making the code more maintainable and the state management more robust.
This commit is contained in:
@@ -13,8 +13,6 @@
|
||||
"@blocksuite/block-std": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lodash-es": "^4.17.21",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -7,7 +7,15 @@ import {
|
||||
} from '@blocksuite/block-std/gfx';
|
||||
import type { Container } from '@blocksuite/global/di';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
distinctUntilChanged,
|
||||
merge,
|
||||
Subject,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
debugLog,
|
||||
@@ -37,7 +45,7 @@ export const TurboRendererConfigFactory =
|
||||
export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
static override key = 'viewportTurboRenderer';
|
||||
|
||||
public state: RenderingState = 'inactive';
|
||||
private readonly state$ = new BehaviorSubject<RenderingState>('inactive');
|
||||
public readonly canvas: HTMLCanvasElement = document.createElement('canvas');
|
||||
private readonly worker: Worker;
|
||||
private readonly disposables = new DisposableGroup();
|
||||
@@ -45,6 +53,7 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
private layoutVersion = 0;
|
||||
private bitmap: ImageBitmap | null = null;
|
||||
private viewportElement: GfxViewportElement | null = null;
|
||||
private readonly refresh$ = new Subject<void>();
|
||||
|
||||
constructor(gfx: GfxController) {
|
||||
super(gfx);
|
||||
@@ -55,6 +64,21 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
throw new Error('TurboRendererConfig not found');
|
||||
}
|
||||
this.worker = config.painterWorkerEntry();
|
||||
|
||||
// Set up state change logging
|
||||
this.state$
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
tap(state => this.debugLog(`State changed to: ${state}`))
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// Set up debounced refresh
|
||||
this.refresh$
|
||||
.pipe(debounceTime(this.options.debounceTime))
|
||||
.subscribe(() => {
|
||||
this.refresh().catch(console.error);
|
||||
});
|
||||
}
|
||||
|
||||
static override extendGfx(gfx: GfxController) {
|
||||
@@ -84,39 +108,45 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
mountPoint.append(this.canvas);
|
||||
}
|
||||
|
||||
const subscription = this.viewport.elementReady.subscribe(element => {
|
||||
subscription.unsubscribe();
|
||||
this.viewport.elementReady.pipe(take(1)).subscribe(element => {
|
||||
this.viewportElement = element;
|
||||
syncCanvasSize(this.canvas, this.std.host);
|
||||
this.setState('pending');
|
||||
this.state$.next('pending');
|
||||
|
||||
this.disposables.add(
|
||||
this.viewport.sizeUpdated.subscribe(() => this.handleResize())
|
||||
);
|
||||
|
||||
this.disposables.add(
|
||||
this.viewport.viewportUpdated.subscribe(() => {
|
||||
this.refresh().catch(console.error);
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.add({
|
||||
dispose: this.viewport.zooming$.subscribe(isZooming => {
|
||||
this.debugLog(`Zooming signal changed: ${isZooming}`);
|
||||
if (isZooming) {
|
||||
this.setState('zooming');
|
||||
} else if (this.state === 'zooming') {
|
||||
this.setState('pending');
|
||||
this.refresh().catch(console.error);
|
||||
}
|
||||
}),
|
||||
});
|
||||
this.disposables.add(
|
||||
this.viewport.zooming$
|
||||
.pipe(
|
||||
tap(isZooming => {
|
||||
this.debugLog(`Zooming signal changed: ${isZooming}`);
|
||||
if (isZooming) {
|
||||
this.state$.next('zooming');
|
||||
} else if (this.state$.value === 'zooming') {
|
||||
this.state$.next('pending');
|
||||
this.refresh().catch(console.error);
|
||||
}
|
||||
})
|
||||
)
|
||||
.subscribe()
|
||||
);
|
||||
});
|
||||
|
||||
// Handle selection and block updates
|
||||
const selectionUpdates$ = this.selection.slots.updated;
|
||||
const blockUpdates$ = this.std.store.slots.blockUpdated;
|
||||
|
||||
// Combine all events that should trigger invalidation
|
||||
this.disposables.add(
|
||||
this.selection.slots.updated.subscribe(() => this.invalidate())
|
||||
);
|
||||
this.disposables.add(
|
||||
this.std.store.slots.blockUpdated.subscribe(() => this.invalidate())
|
||||
merge(selectionUpdates$, blockUpdates$).subscribe(() => this.invalidate())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -127,7 +157,9 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
this.worker.terminate();
|
||||
this.canvas.remove();
|
||||
this.disposables.dispose();
|
||||
this.setState('inactive');
|
||||
this.state$.next('inactive');
|
||||
this.state$.complete();
|
||||
this.refresh$.complete();
|
||||
}
|
||||
|
||||
get viewport() {
|
||||
@@ -146,41 +178,40 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
if (this.state === 'inactive') return;
|
||||
if (this.state$.value === 'inactive') return;
|
||||
|
||||
this.clearCanvas();
|
||||
// -> pending
|
||||
|
||||
// Determine the next state based on current conditions
|
||||
let nextState: RenderingState;
|
||||
|
||||
if (this.viewport.zoom > this.options.zoomThreshold) {
|
||||
this.debugLog('Zoom above threshold, falling back to DOM rendering');
|
||||
this.setState('pending');
|
||||
nextState = 'pending';
|
||||
this.clearOptimizedBlocks();
|
||||
}
|
||||
// -> zooming
|
||||
else if (this.isZooming()) {
|
||||
} else if (this.isZooming()) {
|
||||
this.debugLog('Currently zooming, using placeholder rendering');
|
||||
this.setState('zooming');
|
||||
nextState = 'zooming';
|
||||
this.paintPlaceholder();
|
||||
this.updateOptimizedBlocks();
|
||||
}
|
||||
// -> ready
|
||||
else if (this.canUseBitmapCache()) {
|
||||
} else if (this.canUseBitmapCache()) {
|
||||
this.debugLog('Using cached bitmap');
|
||||
this.setState('ready');
|
||||
nextState = 'ready';
|
||||
this.drawCachedBitmap();
|
||||
this.updateOptimizedBlocks();
|
||||
}
|
||||
// -> rendering
|
||||
else {
|
||||
this.setState('rendering');
|
||||
} else {
|
||||
this.debugLog('Starting bitmap rendering');
|
||||
nextState = 'rendering';
|
||||
this.state$.next(nextState);
|
||||
await this.paintLayout();
|
||||
this.drawCachedBitmap();
|
||||
this.updateOptimizedBlocks();
|
||||
// After rendering completes, transition to ready state
|
||||
nextState = 'ready';
|
||||
}
|
||||
}
|
||||
|
||||
debouncedRefresh = debounce(() => {
|
||||
this.refresh().catch(console.error);
|
||||
}, this.options.debounceTime);
|
||||
this.state$.next(nextState);
|
||||
}
|
||||
|
||||
invalidate() {
|
||||
this.layoutVersion++;
|
||||
@@ -188,13 +219,13 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
this.clearBitmap();
|
||||
this.clearCanvas();
|
||||
this.clearOptimizedBlocks();
|
||||
this.setState('pending');
|
||||
this.state$.next('pending');
|
||||
this.debugLog(`Invalidated renderer (layoutVersion=${this.layoutVersion})`);
|
||||
}
|
||||
|
||||
private debugLog(message: string) {
|
||||
if (!debug) return;
|
||||
debugLog(message, this.state);
|
||||
debugLog(message, this.state$.value);
|
||||
}
|
||||
|
||||
private clearBitmap() {
|
||||
@@ -234,21 +265,21 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
);
|
||||
this.clearBitmap();
|
||||
this.bitmap = e.data.bitmap;
|
||||
this.setState('ready');
|
||||
this.state$.next('ready');
|
||||
resolve();
|
||||
} else {
|
||||
this.debugLog(
|
||||
`Received outdated bitmap (got=${e.data.version}, current=${this.layoutVersion})`
|
||||
);
|
||||
e.data.bitmap.close();
|
||||
this.setState('pending');
|
||||
this.state$.next('pending');
|
||||
resolve();
|
||||
}
|
||||
} else if (e.data.type === 'paintError') {
|
||||
this.debugLog(
|
||||
`Paint error: ${e.data.error} for blockType: ${e.data.blockType}`
|
||||
);
|
||||
this.setState('pending');
|
||||
this.state$.next('pending');
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
@@ -275,7 +306,7 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
private drawCachedBitmap() {
|
||||
if (!this.bitmap) {
|
||||
this.debugLog('No cached bitmap available, requesting refresh');
|
||||
this.debouncedRefresh();
|
||||
this.refresh$.next();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -301,17 +332,11 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
this.debugLog('Bitmap drawn to canvas');
|
||||
}
|
||||
|
||||
setState(newState: RenderingState) {
|
||||
if (this.state === newState) return;
|
||||
this.state = newState;
|
||||
this.debugLog(`State change: ${this.state} -> ${newState}`);
|
||||
}
|
||||
|
||||
private canOptimize(): boolean {
|
||||
const isBelowZoomThreshold =
|
||||
this.viewport.zoom <= this.options.zoomThreshold;
|
||||
return (
|
||||
(this.state === 'ready' || this.state === 'zooming') &&
|
||||
(this.state$.value === 'ready' || this.state$.value === 'zooming') &&
|
||||
isBelowZoomThreshold
|
||||
);
|
||||
}
|
||||
@@ -335,7 +360,7 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
this.debugLog('Container resized, syncing canvas size');
|
||||
syncCanvasSize(this.canvas, this.std.host);
|
||||
this.invalidate();
|
||||
this.debouncedRefresh();
|
||||
this.refresh$.next();
|
||||
}
|
||||
|
||||
private paintPlaceholder() {
|
||||
|
||||
@@ -5,9 +5,8 @@ import {
|
||||
type IVec,
|
||||
Vec,
|
||||
} from '@blocksuite/global/gfx';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { debounceTime, Subject } from 'rxjs';
|
||||
import { BehaviorSubject, debounceTime, Subject } from 'rxjs';
|
||||
|
||||
import type { GfxViewportElement } from '.';
|
||||
|
||||
@@ -100,19 +99,19 @@ export class Viewport {
|
||||
center: IVec;
|
||||
}>();
|
||||
|
||||
zooming$ = signal(false);
|
||||
panning$ = signal(false);
|
||||
zooming$ = new BehaviorSubject<boolean>(false);
|
||||
panning$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
ZOOM_MAX = ZOOM_MAX;
|
||||
|
||||
ZOOM_MIN = ZOOM_MIN;
|
||||
|
||||
private readonly _resetZooming = debounce(() => {
|
||||
this.zooming$.value = false;
|
||||
this.zooming$.next(false);
|
||||
}, 200);
|
||||
|
||||
private readonly _resetPanning = debounce(() => {
|
||||
this.panning$.value = false;
|
||||
this.panning$.next(false);
|
||||
}, 200);
|
||||
|
||||
constructor() {
|
||||
@@ -296,9 +295,8 @@ export class Viewport {
|
||||
this.viewportMoved.complete();
|
||||
this.viewportUpdated.complete();
|
||||
this._resizeSubject.complete();
|
||||
|
||||
this.zooming$.value = false;
|
||||
this.panning$.value = false;
|
||||
this.zooming$.complete();
|
||||
this.panning$.complete();
|
||||
}
|
||||
|
||||
getFitToScreenData(
|
||||
@@ -371,7 +369,7 @@ export class Viewport {
|
||||
|
||||
this._center.x = centerX;
|
||||
this._center.y = centerY;
|
||||
this.panning$.value = true;
|
||||
this.panning$.next(true);
|
||||
this.viewportUpdated.next({
|
||||
zoom: this.zoom,
|
||||
center: Vec.toVec(this.center) as IVec,
|
||||
@@ -530,7 +528,7 @@ export class Viewport {
|
||||
Vec.mul(offset, prevZoom / newZoom)
|
||||
);
|
||||
if (wheel) {
|
||||
this.zooming$.value = true;
|
||||
this.zooming$.next(true);
|
||||
}
|
||||
this.setCenter(newCenter[0], newCenter[1], forceUpdate);
|
||||
this.viewportUpdated.next({
|
||||
|
||||
Reference in New Issue
Block a user