mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-19 02:51:47 +08:00
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. [](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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user