feat(native): async recorder (#14700)

#### PR Dependency Tree


* **PR #14700** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

* **New Features**
* Durable, resumable import queue with explicit import lifecycle and
updated popup/tray status behavior.
* Async native recording APIs and ability to abort recordings; audio
quality metrics (degraded, overflow count).
  * Added "Importing..." translation.

* **Bug Fixes**
* More reliable single-claim import processing, retries and cleanup to
avoid duplicate imports.
  * Improved stop/abort teardown stability and safer shutdown behavior.

* **Tests**
* New/updated tests covering coordinator, import queue, native async
flows and teardown scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-03-23 22:38:52 +08:00
committed by GitHub
parent b0251c93cb
commit 9b56a05159
26 changed files with 3560 additions and 1277 deletions
@@ -10,8 +10,17 @@ import * as styles from './styles.css';
type Status = {
id: number;
status: 'new' | 'recording' | 'processing' | 'ready';
blockCreationStatus?: 'success' | 'failed';
status:
| 'new'
| 'starting'
| 'start_failed'
| 'recording'
| 'finalizing'
| 'pending_import'
| 'importing'
| 'imported'
| 'import_failed'
| 'finalize_failed';
appName?: string;
appGroupId?: number;
icon?: Buffer;
@@ -56,19 +65,18 @@ export function Recording() {
}
if (status.status === 'new') {
return t['com.affine.recording.new']();
} else if (
status.status === 'ready' &&
status.blockCreationStatus === 'success'
) {
} else if (status.status === 'imported') {
return t['com.affine.recording.success.prompt']();
} else if (
status.status === 'ready' &&
status.blockCreationStatus === 'failed'
status.status === 'import_failed' ||
status.status === 'start_failed' ||
status.status === 'finalize_failed'
) {
return t['com.affine.recording.failed.prompt']();
} else if (
status.status === 'starting' ||
status.status === 'recording' ||
status.status === 'processing'
status.status === 'finalizing'
) {
if (status.appName) {
return t['com.affine.recording.recording']({
@@ -77,11 +85,19 @@ export function Recording() {
} else {
return t['com.affine.recording.recording.unnamed']();
}
} else if (
status.status === 'pending_import' ||
status.status === 'importing'
) {
return t['com.affine.recording.importing.prompt']();
}
return null;
}, [status, t]);
const handleDismiss = useAsyncCallback(async () => {
if (status) {
await apis?.recording?.dismissRecordingStatus(status.id);
}
await apis?.popup?.dismissCurrentRecording();
track.popup.$.recordingBar.dismissRecording({
type: 'Meeting record',
@@ -155,8 +171,10 @@ export function Recording() {
</Button>
);
} else if (
status.status === 'processing' ||
(status.status === 'ready' && !status.blockCreationStatus)
status.status === 'starting' ||
status.status === 'finalizing' ||
status.status === 'pending_import' ||
status.status === 'importing'
) {
return (
<Button
@@ -166,18 +184,21 @@ export function Recording() {
disabled
/>
);
} else if (
status.status === 'ready' &&
status.blockCreationStatus === 'success'
) {
} else if (status.status === 'imported') {
return (
<Button variant="primary" onClick={handleDismiss}>
{t['com.affine.recording.success.button']()}
</Button>
);
} else if (status.status === 'start_failed') {
return (
<Button variant="plain" onClick={handleDismiss}>
{t['com.affine.recording.dismiss']()}
</Button>
);
} else if (
status.status === 'ready' &&
status.blockCreationStatus === 'failed'
status.status === 'import_failed' ||
status.status === 'finalize_failed'
) {
return (
<>