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 -->
This commit is contained in:
EYHN
2025-06-25 10:55:27 +08:00
committed by GitHub
parent 6813d84deb
commit aa4874a55c
21 changed files with 366 additions and 178 deletions

View File

@@ -18,9 +18,7 @@ import {
type DocRecord,
type DocStorage,
type DocUpdate,
type IndexerDocument,
type IndexerSchema,
type IndexerStorage,
type ListedBlobRecord,
type Query,
type SearchOptions,
@@ -29,7 +27,7 @@ import {
import type { AwarenessSync } from '../sync/awareness';
import type { BlobSync } from '../sync/blob';
import type { DocSync } from '../sync/doc';
import type { IndexerSync } from '../sync/indexer';
import type { IndexerPreferOptions, IndexerSync } from '../sync/indexer';
import type { StoreInitOptions, WorkerManagerOps, WorkerOps } from './ops';
export type { StoreInitOptions as WorkerInitOptions } from './ops';
@@ -100,12 +98,8 @@ export class StoreClient {
this.docFrontend = new DocFrontend(this.docStorage, this.docSync);
this.blobFrontend = new BlobFrontend(this.blobStorage, this.blobSync);
this.awarenessFrontend = new AwarenessFrontend(this.awarenessSync);
this.indexerStorage = new WorkerIndexerStorage(this.client);
this.indexerSync = new WorkerIndexerSync(this.client);
this.indexerFrontend = new IndexerFrontend(
this.indexerStorage,
this.indexerSync
);
this.indexerFrontend = new IndexerFrontend(this.indexerSync);
}
private readonly docStorage: WorkerDocStorage;
@@ -113,7 +107,6 @@ export class StoreClient {
private readonly docSync: WorkerDocSync;
private readonly blobSync: WorkerBlobSync;
private readonly awarenessSync: WorkerAwarenessSync;
private readonly indexerStorage: WorkerIndexerStorage;
private readonly indexerSync: WorkerIndexerSync;
readonly docFrontend: DocFrontend;
@@ -348,26 +341,23 @@ class WorkerAwarenessSync implements AwarenessSync {
}
}
class WorkerIndexerStorage implements IndexerStorage {
class WorkerIndexerSync implements IndexerSync {
constructor(private readonly client: OpClient<WorkerOps>) {}
readonly storageType = 'indexer';
readonly isReadonly = true;
connection = new WorkerIndexerConnection(this.client);
search<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
table: T,
query: Query<T>,
options?: O
options?: O & { prefer?: IndexerPreferOptions }
): Promise<SearchResult<T, O>> {
return this.client.call('indexerStorage.search', { table, query, options });
return this.client.call('indexerSync.search', { table, query, options });
}
aggregate<T extends keyof IndexerSchema, const O extends AggregateOptions<T>>(
table: T,
query: Query<T>,
field: keyof IndexerSchema[T],
options?: O
options?: O & { prefer?: IndexerPreferOptions }
): Promise<AggregateResult<T, O>> {
return this.client.call('indexerStorage.aggregate', {
return this.client.call('indexerSync.aggregate', {
table,
query,
field: field as string,
@@ -377,9 +367,9 @@ class WorkerIndexerStorage implements IndexerStorage {
search$<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
table: T,
query: Query<T>,
options?: O
options?: O & { prefer?: IndexerPreferOptions }
): Observable<SearchResult<T, O>> {
return this.client.ob$('indexerStorage.subscribeSearch', {
return this.client.ob$('indexerSync.subscribeSearch', {
table,
query,
options,
@@ -392,59 +382,16 @@ class WorkerIndexerStorage implements IndexerStorage {
table: T,
query: Query<T>,
field: keyof IndexerSchema[T],
options?: O
options?: O & { prefer?: IndexerPreferOptions }
): Observable<AggregateResult<T, O>> {
return this.client.ob$('indexerStorage.subscribeAggregate', {
return this.client.ob$('indexerSync.subscribeAggregate', {
table,
query,
field: field as string,
options,
});
}
deleteByQuery<T extends keyof IndexerSchema>(
_table: T,
_query: Query<T>
): Promise<void> {
throw new Error('Method not implemented.');
}
insert<T extends keyof IndexerSchema>(
_table: T,
_document: IndexerDocument<T>
): Promise<void> {
throw new Error('Method not implemented.');
}
delete<T extends keyof IndexerSchema>(_table: T, _id: string): Promise<void> {
throw new Error('Method not implemented.');
}
update<T extends keyof IndexerSchema>(
_table: T,
_document: IndexerDocument<T>
): Promise<void> {
throw new Error('Method not implemented.');
}
refresh<T extends keyof IndexerSchema>(_table: T): Promise<void> {
throw new Error('Method not implemented.');
}
}
class WorkerIndexerConnection extends DummyConnection {
constructor(private readonly client: OpClient<WorkerOps>) {
super();
}
promise: Promise<void> | undefined;
override waitForConnected(): Promise<void> {
if (this.promise) {
return this.promise;
}
this.promise = this.client.call('indexerStorage.waitForConnected');
return this.promise;
}
}
class WorkerIndexerSync implements IndexerSync {
constructor(private readonly client: OpClient<WorkerOps>) {}
waitForCompleted(signal?: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
const abortListener = () => {