Files
AFFiNE-Mirror/packages/common/infra/src/op/client.ts
T
EYHN aa4874a55c feat(core): use cloud indexer for search (#12899)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added enhanced error handling and user-friendly error messages in
quick search and document search menus.
  - Introduced loading state indicators for search operations.
  - Quick Search now provides explicit error feedback in the UI.

- **Improvements**
- Search and aggregation operations can now prefer remote or local
indexers based on user or system preference.
- Streamlined indexer logic for more consistent and reliable search
experiences.
- Refined error handling in messaging and synchronization layers for
improved stability.
- Enhanced error object handling in messaging for clearer error
propagation.
- Updated cloud workspace storage to always use IndexedDB locally and
CloudIndexer remotely.
- Shifted indexer operations to use synchronized indexer layer for
better consistency.
  - Simplified indexer client by consolidating storage and sync layers.
- Improved error propagation in messaging handlers by wrapping error
objects.
- Updated document search to prioritize remote indexer results by
default.

- **Bug Fixes**
- Improved robustness of search features by handling errors gracefully
and preventing potential runtime issues.

- **Style**
  - Added new styles for displaying error messages in search interfaces.

- **Chores**
- Removed the obsolete "Enable Cloud Indexer" feature flag; cloud
indexer behavior is now always enabled where applicable.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-25 02:55:27 +00:00

219 lines
5.1 KiB
TypeScript

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,
OpInputWithSignal,
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;
}
export 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: Infinity,
};
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(Object.assign(new Error(), 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(Object.assign(new 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: OpInputWithSignal<Ops, Op>
): CancelablePromise<OpOutput<Ops, Op>> {
const promiseWithResolvers = Promise.withResolvers<any>();
const abortSignal =
args[args.length - 1] instanceof AbortSignal
? (args.pop() as AbortSignal)
: undefined;
const payload = args.pop();
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: any) => {
const pending = this.pendingCalls.get(msg.id);
if (!pending) {
return;
}
this.port.postMessage({
type: 'cancel',
id: msg.id,
} satisfies CancelMessage);
promiseWithResolvers.reject(reason);
clearTimeout(pending.timeout);
this.pendingCalls.delete(msg.id);
};
abortSignal?.addEventListener('abort', () => {
raise(abortSignal.reason);
});
promise.cancel = () => {
raise('canceled');
};
const timeout =
this.options.timeout === Infinity
? 0
: 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;
}
ob$<Op extends OpNames<Ops>, Out extends OpOutput<Ops, Op>>(
op: Op,
...args: OpInput<Ops, Op>
): Observable<Out> {
const sub$ = new Observable<Out>(ob => {
const payload = args[0];
const msg = {
type: 'subscribe',
id: this.nextCallId(op),
name: op as string,
payload,
} satisfies SubscribeMessage;
const transferables = fetchTransferables(payload);
this.port.postMessage(msg, { transfer: transferables });
this.obs.set(msg.id, ob);
return () => {
ob.complete();
this.obs.delete(msg.id);
this.port.postMessage({
type: 'unsubscribe',
id: msg.id,
} satisfies UnsubscribeMessage);
};
});
return sub$;
}
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();
}
}