Files
AFFiNE/packages/backend/server/src/plugins/copilot/context/realtime.ts
DarkSky db0ff0a9df 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 -->
2026-05-11 00:33:25 +08:00

109 lines
3.6 KiB
TypeScript

import { Injectable, OnModuleInit, Optional } from '@nestjs/common';
import { z } from 'zod';
import { OnEvent } from '../../../base';
import { AccessController } from '../../../core/permission';
import {
RealtimePublisher,
RealtimeRegistry,
realtimeWorkspaceEmbeddingProgressRoom,
registerRealtimeLiveQuery,
} from '../../../core/realtime';
import { Models } from '../../../models';
import { CopilotContextService } from './service';
export function workspaceEmbeddingRoom(workspaceId: string) {
return realtimeWorkspaceEmbeddingProgressRoom(workspaceId);
}
@Injectable()
export class CopilotEmbeddingRealtimeProvider implements OnModuleInit {
constructor(
private readonly ac: AccessController,
private readonly models: Models,
private readonly context: CopilotContextService,
@Optional() private readonly registry?: RealtimeRegistry,
@Optional() private readonly publisher?: RealtimePublisher
) {}
onModuleInit() {
const input = z.object({ workspaceId: z.string() });
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
);
},
},
topic: {
name: 'workspace.embedding.progress.changed',
input,
authorize: async (user, payload) => {
await this.assertCopilot(user.id, payload.workspaceId);
},
room: (_user, payload) => workspaceEmbeddingRoom(payload.workspaceId),
},
});
}
@OnEvent('workspace.doc.embed.finished', { suppressError: true })
async onDocEmbedFinished(payload: Events['workspace.doc.embed.finished']) {
await this.publishContext(payload.contextId, 'finished');
}
@OnEvent('workspace.doc.embed.failed', { suppressError: true })
async onDocEmbedFailed(payload: Events['workspace.doc.embed.failed']) {
await this.publishContext(payload.contextId, 'failed');
}
@OnEvent('workspace.file.embed.finished', { suppressError: true })
async onFileEmbedFinished(payload: Events['workspace.file.embed.finished']) {
await this.publishContext(payload.contextId, 'finished');
}
@OnEvent('workspace.file.embed.failed', { suppressError: true })
async onFileEmbedFailed(payload: Events['workspace.file.embed.failed']) {
await this.publishContext(payload.contextId, 'failed');
}
@OnEvent('workspace.blob.embed.finished', { suppressError: true })
async onBlobEmbedFinished(payload: Events['workspace.blob.embed.finished']) {
await this.publishContext(payload.contextId, 'finished');
}
@OnEvent('workspace.blob.embed.failed', { suppressError: true })
async onBlobEmbedFailed(payload: Events['workspace.blob.embed.failed']) {
await this.publishContext(payload.contextId, 'failed');
}
private async publishContext(
contextId: string,
reason: 'finished' | 'failed'
) {
if (!this.publisher) return;
const context = await this.context.get(contextId);
this.publisher.publish(
'workspace.embedding.progress.changed',
{ workspaceId: context.workspaceId },
{ reason },
{ room: workspaceEmbeddingRoom(context.workspaceId) }
);
}
private async assertCopilot(userId: string, workspaceId: string) {
await this.ac
.user(userId)
.workspace(workspaceId)
.allowLocal()
.assert('Workspace.Copilot');
}
}