feat(core): migrate more pull to realtime (#14936)

#### PR Dependency Tree


* **PR #14936** 👈

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

* **Refactor**
* Consolidated realtime subscription patterns for consistent, more
reliable live updates across comments, notifications, transcription
tasks, and embedding progress.
* Standardized realtime room naming and subscription keys for
deterministic delivery.

* **New Features**
* Introduced a reusable live-query mechanism powering realtime snapshot
+ event workflows used by comments, notifications, transcript tasks, and
embedding progress.

* **Tests**
* Added tests covering live-query behavior and deterministic
subscription key generation.

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/toeverything/AFFiNE/pull/14936)
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-05-11 00:33:25 +08:00
committed by GitHub
parent 8cf00738c2
commit db0ff0a9df
26 changed files with 812 additions and 441 deletions
@@ -3,15 +3,17 @@ import { z } from 'zod';
import { OnEvent } from '../../../base';
import { AccessController } from '../../../core/permission';
import type {
import {
RealtimePublisher,
RealtimeRegistry,
realtimeWorkspaceEmbeddingProgressRoom,
registerRealtimeLiveQuery,
} from '../../../core/realtime';
import { Models } from '../../../models';
import { CopilotContextService } from './service';
export function workspaceEmbeddingRoom(workspaceId: string) {
return `workspace:${workspaceId}:embedding-progress`;
return realtimeWorkspaceEmbeddingProgressRoom(workspaceId);
}
@Injectable()
@@ -27,27 +29,28 @@ export class CopilotEmbeddingRealtimeProvider implements OnModuleInit {
onModuleInit() {
const input = z.object({ workspaceId: z.string() });
this.registry?.registerRequest({
name: 'workspace.embedding.progress.get',
input,
handle: async (user, payload) => {
await this.assertCopilot(user.id, payload.workspaceId);
if (!this.context.canEmbedding) {
return { total: 0, embedded: 0 };
}
return await this.models.copilotWorkspace.getEmbeddingStatus(
payload.workspaceId
);
registerRealtimeLiveQuery(this.registry, {
request: {
name: 'workspace.embedding.progress.get',
input,
handle: async (user, payload) => {
await this.assertCopilot(user.id, payload.workspaceId);
if (!this.context.canEmbedding) {
return { total: 0, embedded: 0 };
}
return await this.models.copilotWorkspace.getEmbeddingStatus(
payload.workspaceId
);
},
},
});
this.registry?.registerTopic({
name: 'workspace.embedding.progress.changed',
input,
authorize: async (user, payload) => {
await this.assertCopilot(user.id, payload.workspaceId);
topic: {
name: 'workspace.embedding.progress.changed',
input,
authorize: async (user, payload) => {
await this.assertCopilot(user.id, payload.workspaceId);
},
room: (_user, payload) => workspaceEmbeddingRoom(payload.workspaceId),
},
room: (_user, payload) => workspaceEmbeddingRoom(payload.workspaceId),
});
}
@@ -3,8 +3,12 @@ import { z } from 'zod';
import { CopilotTranscriptionJobNotFound } from '../../../base';
import { AccessController } from '../../../core/permission';
import type { RealtimeRegistry } from '../../../core/realtime';
import { CopilotTranscriptionService, transcriptTaskRoom } from './service';
import {
type RealtimeRegistry,
realtimeTranscriptTaskRoom,
registerRealtimeLiveQuery,
} from '../../../core/realtime';
import { CopilotTranscriptionService } from './service';
@Injectable()
export class CopilotTranscriptRealtimeProvider implements OnModuleInit {
@@ -15,47 +19,51 @@ export class CopilotTranscriptRealtimeProvider implements OnModuleInit {
) {}
onModuleInit() {
this.registry?.registerRequest({
name: 'copilot.transcript.task.get',
input: z
.object({
workspaceId: z.string(),
blobId: z.string().optional(),
taskId: z.string().optional(),
})
.refine(input => input.blobId || input.taskId),
handle: async (user, input) => {
await this.assertCopilot(user.id, input.workspaceId);
return {
task: await this.transcript.queryTask(
user.id,
input.workspaceId,
input.taskId,
input.blobId
),
};
},
const requestInput = z
.object({
workspaceId: z.string(),
blobId: z.string().optional(),
taskId: z.string().optional(),
})
.refine(input => input.blobId || input.taskId);
const topicInput = z.object({
workspaceId: z.string(),
taskId: z.string(),
});
this.registry?.registerTopic({
name: 'copilot.transcript.task.changed',
input: z.object({
workspaceId: z.string(),
taskId: z.string(),
}),
authorize: async (user, input) => {
await this.assertCopilot(user.id, input.workspaceId);
const task = await this.transcript.queryTask(
user.id,
input.workspaceId,
input.taskId
);
if (!task) {
throw new CopilotTranscriptionJobNotFound();
}
registerRealtimeLiveQuery(this.registry, {
request: {
name: 'copilot.transcript.task.get',
input: requestInput,
handle: async (user, input) => {
await this.assertCopilot(user.id, input.workspaceId);
return {
task: await this.transcript.queryTask(
user.id,
input.workspaceId,
input.taskId,
input.blobId
),
};
},
},
topic: {
name: 'copilot.transcript.task.changed',
input: topicInput,
authorize: async (user, input) => {
await this.assertCopilot(user.id, input.workspaceId);
const task = await this.transcript.queryTask(
user.id,
input.workspaceId,
input.taskId
);
if (!task) {
throw new CopilotTranscriptionJobNotFound();
}
},
room: (_user, input) =>
realtimeTranscriptTaskRoom(input.workspaceId, input.taskId),
},
room: (_user, input) =>
transcriptTaskRoom(input.workspaceId, input.taskId),
});
}
@@ -9,7 +9,10 @@ import {
OnJob,
sniffMime,
} from '../../../base';
import type { RealtimePublisher } from '../../../core/realtime';
import {
type RealtimePublisher,
realtimeTranscriptTaskRoom,
} from '../../../core/realtime';
import { Models } from '../../../models';
import { CopilotAccessPolicy } from '../access';
import { PromptService } from '../prompt';
@@ -33,10 +36,6 @@ const TRANSCRIPT_ACTION_ID = 'transcript.audio.gemini';
const TRANSCRIPT_ACTION_VERSION = 'v1';
const TRANSCRIPT_STRATEGY = 'gemini';
export function transcriptTaskRoom(workspaceId: string, taskId: string) {
return `copilot:transcript:${workspaceId}:${taskId}`;
}
export type TranscriptionJob = {
id: string;
status: AiJobStatus;
@@ -468,7 +467,7 @@ export class CopilotTranscriptionService {
'copilot.transcript.task.changed',
{ workspaceId, taskId },
{ taskId, status, error },
{ room: transcriptTaskRoom(workspaceId, taskId) }
{ room: realtimeTranscriptTaskRoom(workspaceId, taskId) }
);
}
}