mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-23 21:06:22 +08:00
feat(infra): introduce op pattern (#8734)
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import { merge } from 'lodash-es';
|
||||
import { Observable, type Observer } from 'rxjs';
|
||||
|
||||
import {
|
||||
AutoMessageHandler,
|
||||
type CallMessage,
|
||||
type CancelMessage,
|
||||
fetchTransferables,
|
||||
type MessageCommunicapable,
|
||||
type MessageHandlers,
|
||||
type SubscribeMessage,
|
||||
type UnsubscribeMessage,
|
||||
} from './message';
|
||||
import type { OpInput, OpNames, OpOutput, OpSchema } from './types';
|
||||
|
||||
export interface CancelablePromise<T> extends Promise<T> {
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
interface PendingCall extends PromiseWithResolvers<any> {
|
||||
id: string;
|
||||
timeout: number | NodeJS.Timeout;
|
||||
}
|
||||
|
||||
interface OpClientOptions {
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export class OpClient<Ops extends OpSchema> extends AutoMessageHandler {
|
||||
private readonly callIds = new Map<OpNames<Ops>, number>();
|
||||
private readonly pendingCalls = new Map<string, PendingCall>();
|
||||
private readonly obs = new Map<string, Observer<any>>();
|
||||
private readonly options: OpClientOptions = {
|
||||
timeout: 3000,
|
||||
};
|
||||
|
||||
constructor(port: MessageCommunicapable, options: OpClientOptions = {}) {
|
||||
super(port);
|
||||
merge(this.options, options);
|
||||
}
|
||||
|
||||
protected override get handlers() {
|
||||
return {
|
||||
return: this.handleReturnMessage,
|
||||
next: this.handleSubscriptionNextMessage,
|
||||
error: this.handleSubscriptionErrorMessage,
|
||||
complete: this.handleSubscriptionCompleteMessage,
|
||||
};
|
||||
}
|
||||
|
||||
private readonly handleReturnMessage: MessageHandlers['return'] = msg => {
|
||||
const pending = this.pendingCalls.get(msg.id);
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ('error' in msg) {
|
||||
pending.reject(msg.error);
|
||||
} else {
|
||||
pending.resolve(msg.data);
|
||||
}
|
||||
clearTimeout(pending.timeout);
|
||||
this.pendingCalls.delete(msg.id);
|
||||
};
|
||||
|
||||
private readonly handleSubscriptionNextMessage: MessageHandlers['next'] =
|
||||
msg => {
|
||||
const ob = this.obs.get(msg.id);
|
||||
if (!ob) {
|
||||
return;
|
||||
}
|
||||
|
||||
ob.next(msg.data);
|
||||
};
|
||||
|
||||
private readonly handleSubscriptionErrorMessage: MessageHandlers['error'] =
|
||||
msg => {
|
||||
const ob = this.obs.get(msg.id);
|
||||
if (!ob) {
|
||||
return;
|
||||
}
|
||||
|
||||
ob.error(msg.error);
|
||||
};
|
||||
|
||||
private readonly handleSubscriptionCompleteMessage: MessageHandlers['complete'] =
|
||||
msg => {
|
||||
const ob = this.obs.get(msg.id);
|
||||
if (!ob) {
|
||||
return;
|
||||
}
|
||||
|
||||
ob.complete();
|
||||
};
|
||||
|
||||
protected nextCallId(op: OpNames<Ops>) {
|
||||
let id = this.callIds.get(op) ?? 0;
|
||||
id++;
|
||||
this.callIds.set(op, id);
|
||||
|
||||
return `${op}:${id}`;
|
||||
}
|
||||
|
||||
protected currentCallId(op: OpNames<Ops>) {
|
||||
return this.callIds.get(op) ?? 0;
|
||||
}
|
||||
|
||||
call<Op extends OpNames<Ops>>(
|
||||
op: Op,
|
||||
...args: OpInput<Ops, Op>
|
||||
): CancelablePromise<OpOutput<Ops, Op>> {
|
||||
const promiseWithResolvers = Promise.withResolvers<any>();
|
||||
const payload = args[0];
|
||||
|
||||
const msg = {
|
||||
type: 'call',
|
||||
id: this.nextCallId(op),
|
||||
name: op as string,
|
||||
payload,
|
||||
} satisfies CallMessage;
|
||||
|
||||
const promise = promiseWithResolvers.promise as CancelablePromise<any>;
|
||||
|
||||
const raise = (reason: string) => {
|
||||
const pending = this.pendingCalls.get(msg.id);
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
this.port.postMessage({
|
||||
type: 'cancel',
|
||||
id: msg.id,
|
||||
} satisfies CancelMessage);
|
||||
promiseWithResolvers.reject(new Error(reason));
|
||||
clearTimeout(pending.timeout);
|
||||
this.pendingCalls.delete(msg.id);
|
||||
};
|
||||
|
||||
promise.cancel = () => {
|
||||
raise('canceled');
|
||||
};
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
raise('timeout');
|
||||
}, this.options.timeout);
|
||||
|
||||
const transferables = fetchTransferables(payload);
|
||||
|
||||
this.port.postMessage(msg, { transfer: transferables });
|
||||
this.pendingCalls.set(msg.id, {
|
||||
...promiseWithResolvers,
|
||||
timeout,
|
||||
id: msg.id,
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
subscribe<Op extends OpNames<Ops>, Out extends OpOutput<Ops, Op>>(
|
||||
op: Op,
|
||||
...args: [
|
||||
...OpInput<Ops, Op>,
|
||||
Partial<Observer<Out>> | ((value: Out) => void),
|
||||
]
|
||||
): () => void {
|
||||
const payload = args[0];
|
||||
const observer = args[1] as Partial<Observer<Out>> | ((value: Out) => void);
|
||||
|
||||
const msg = {
|
||||
type: 'subscribe',
|
||||
id: this.nextCallId(op),
|
||||
name: op as string,
|
||||
payload,
|
||||
} satisfies SubscribeMessage;
|
||||
|
||||
const sub = new Observable<Out>(ob => {
|
||||
this.obs.set(msg.id, ob);
|
||||
}).subscribe(observer);
|
||||
|
||||
sub.add(() => {
|
||||
this.obs.delete(msg.id);
|
||||
this.port.postMessage({
|
||||
type: 'unsubscribe',
|
||||
id: msg.id,
|
||||
} satisfies UnsubscribeMessage);
|
||||
});
|
||||
|
||||
const transferables = fetchTransferables(payload);
|
||||
this.port.postMessage(msg, { transfer: transferables });
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.close();
|
||||
this.pendingCalls.forEach(call => {
|
||||
call.reject(new Error('client destroyed'));
|
||||
});
|
||||
this.pendingCalls.clear();
|
||||
this.obs.forEach(ob => {
|
||||
ob.complete();
|
||||
});
|
||||
this.obs.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user