feat(server): realtime notification & task status (#14934)

#### PR Dependency Tree


* **PR #14934** 👈

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**
* Full realtime platform added: live notifications, comments, embedding
progress, and transcription task updates via realtime subscriptions.

* **Chores**
* Frontend switched from polling/GraphQL queries to realtime channels;
legacy query fields marked deprecated and client libs updated to use
realtime APIs.

[![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/14934)
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


#### PR Dependency Tree


* **PR #14934** 👈
  * **PR #14936**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
DarkSky
2026-05-10 23:21:50 +08:00
committed by GitHub
parent 417d31cabe
commit 8cf00738c2
70 changed files with 2378 additions and 283 deletions
@@ -1,3 +1,14 @@
import type {
RealtimeConfigureInput,
RealtimeRequestInputOf,
RealtimeRequestName,
RealtimeRequestOutputOf,
RealtimeStatus,
RealtimeSubscriptionReady,
RealtimeTopicEventOf,
RealtimeTopicInputOf,
RealtimeTopicName,
} from '@affine/realtime';
import { OpClient, transfer } from '@toeverything/infra/op';
import type { Observable } from 'rxjs';
import { v4 as uuid } from 'uuid';
@@ -38,6 +49,30 @@ import type { StoreInitOptions, WorkerManagerOps, WorkerOps } from './ops';
export type { StoreInitOptions as WorkerInitOptions } from './ops';
type RealtimeWorkerClient = {
call<Op extends RealtimeRequestName>(
name: 'realtime.request',
payload: {
op: Op;
input: RealtimeRequestInputOf<Op>;
timeoutMs?: number;
}
): Promise<RealtimeRequestOutputOf<Op>>;
ob$<Topic extends RealtimeTopicName>(
name: 'realtime.subscribe',
payload: {
topic: Topic;
input: RealtimeTopicInputOf<Topic>;
}
): Observable<RealtimeTopicEventOf<Topic> | RealtimeSubscriptionReady>;
};
function realtimeAbortError(op: RealtimeRequestName) {
const error = new Error(`Realtime request aborted: ${op}`);
error.name = 'AbortError';
return error;
}
export class StoreManagerClient {
private readonly connections = new Map<
string,
@@ -49,9 +84,11 @@ export class StoreManagerClient {
constructor(private readonly client: OpClient<WorkerManagerOps>) {
this.telemetry = new TelemetryClient(this.client);
this.realtime = new RealtimeClient(this.client);
}
readonly telemetry: TelemetryClient;
readonly realtime: RealtimeClient;
open(key: string, options: StoreInitOptions) {
const { port1, port2 } = new MessageChannel();
@@ -138,6 +175,62 @@ class TelemetryClient {
}
}
export class RealtimeClient {
constructor(private readonly client: OpClient<WorkerManagerOps>) {}
configure(context: RealtimeConfigureInput): Promise<void> {
return this.client.call('realtime.configure', context);
}
request<Op extends RealtimeRequestName>(
op: Op,
input: RealtimeRequestInputOf<Op>,
options?: { timeoutMs?: number; signal?: AbortSignal }
): Promise<RealtimeRequestOutputOf<Op>> {
const request = (this.client as unknown as RealtimeWorkerClient).call(
'realtime.request',
{
op,
input,
timeoutMs: options?.timeoutMs,
}
);
if (!options?.signal) {
return request;
}
if (options.signal.aborted) {
return Promise.reject(realtimeAbortError(op));
}
let abortHandler: (() => void) | undefined;
const aborted = new Promise<never>((_resolve, reject) => {
abortHandler = () => reject(realtimeAbortError(op));
options.signal?.addEventListener('abort', abortHandler, { once: true });
});
return Promise.race([request, aborted]).finally(() => {
if (abortHandler) {
options.signal?.removeEventListener('abort', abortHandler);
}
});
}
subscribe<Topic extends RealtimeTopicName>(
topic: Topic,
input: RealtimeTopicInputOf<Topic>
): Observable<RealtimeTopicEventOf<Topic> | RealtimeSubscriptionReady> {
return (this.client as unknown as RealtimeWorkerClient).ob$(
'realtime.subscribe',
{
topic,
input,
}
);
}
status(): Promise<RealtimeStatus> {
return this.client.call('realtime.status');
}
}
export class StoreClient {
constructor(private readonly client: OpClient<WorkerOps>) {
this.docStorage = new WorkerDocStorage(this.client);