Files
AFFiNE-Mirror/blocksuite/playground/apps/_common/sync/blob/mock-server.ts
fundon 93b1d6c729 fix(editor): improve image block upload and download states (#12017)
Related to: [BS-3143](https://linear.app/affine-design/issue/BS-3143/更新-loading-和错误样式)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a unified resource controller for managing image and attachment resources, providing improved loading, error, and state handling.
  - Added a visual loading indicator overlay to image blocks for better feedback during image loading.

- **Improvements**
  - Simplified and centralized image and attachment state management, reducing redundant properties and manual state tracking.
  - Updated fallback UI for image blocks with clearer titles, descriptions, and improved layout.
  - Enhanced batch image block creation and download handling for improved efficiency.
  - Refined image block accessibility with improved alt text and streamlined rendering logic.
  - Centralized target model selection for image insertion in AI actions.
  - Reordered CSS declarations without affecting styling.
  - Improved reactive state tracking for blob upload/download operations in mock server.

- **Bug Fixes**
  - Improved cleanup of object URLs to prevent resource leaks.
  - Adjusted toolbar logic to more accurately reflect available actions based on image state.

- **Tests**
  - Updated end-to-end tests to match new UI text and behaviors for image loading and error states.

- **Chores**
  - Refactored internal logic and updated comments for clarity and maintainability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-07 05:15:57 +00:00

124 lines
2.9 KiB
TypeScript

import type { BlobSource, BlobState } from '@blocksuite/sync';
import { BehaviorSubject, ReplaySubject, share, throttleTime } from 'rxjs';
/**
* @internal just for test
*
* API: /api/collection/:id/blob/:key
* GET: get blob
* PUT: set blob
* DELETE: delete blob
*/
export class MockServerBlobSource implements BlobSource {
private readonly _cache = new Map<string, Blob>();
private readonly _states = new Map<string, BehaviorSubject<BlobState>>();
readonly = false;
constructor(readonly name: string) {}
async delete(key: string) {
this._cache.delete(key);
this._states.delete(key);
await fetch(`/api/collection/${this.name}/blob/${key}`, {
method: 'DELETE',
});
}
async get(key: string) {
if (this._cache.has(key)) return this._cache.get(key)!;
let state$ = this._states.get(key);
if (!state$) {
state$ = new BehaviorSubject<BlobState>(defaultState());
this._states.set(key, state$);
}
let blob: Blob | null = null;
nextState(state$, { downloading: true });
try {
const resp = await fetch(`/api/collection/${this.name}/blob/${key}`);
if (!resp.ok) throw new Error(`Failed to fetch blob ${key}`);
blob = await resp.blob();
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
nextState(state$, { errorMessage });
} finally {
nextState(state$, { downloading: false });
if (blob) {
this._cache.set(key, blob);
}
}
return blob;
}
async list() {
return Array.from(this._cache.keys());
}
async set(key: string, value: Blob) {
let state$ = this._states.get(key);
if (!state$) {
state$ = new BehaviorSubject<BlobState>(defaultState());
this._states.set(key, state$);
}
this._cache.set(key, value);
nextState(state$, { uploading: true });
try {
await fetch(`/api/collection/${this.name}/blob/${key}`, {
method: 'PUT',
body: value,
});
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
nextState(state$, { errorMessage });
} finally {
nextState(state$, { uploading: false });
}
return key;
}
blobState$(key: string) {
let state$ = this._states.get(key);
if (!state$) {
state$ = new BehaviorSubject<BlobState>(defaultState());
this._states.set(key, state$);
nextState(state$, { errorMessage: 'Blob not found' });
}
return state$.pipe(
throttleTime(1000, undefined, { leading: true, trailing: true }),
share({
connector: () => new ReplaySubject(1),
})
);
}
}
function defaultState(): BlobState {
return { uploading: false, downloading: false, overSize: false };
}
function nextState(
state$: BehaviorSubject<BlobState>,
state?: Partial<BlobState>
) {
state$.next({ ...state$.value, ...state });
}