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:
parent
6813d84deb
commit
aa4874a55c
@ -43,7 +43,10 @@ describe('op consumer', () => {
|
|||||||
expect(ctx.postMessage.mock.lastCall).toMatchInlineSnapshot(`
|
expect(ctx.postMessage.mock.lastCall).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"error": [Error: Handler for operation [add] is not registered.],
|
"error": {
|
||||||
|
"message": "Handler for operation [add] is not registered.",
|
||||||
|
"name": "Error",
|
||||||
|
},
|
||||||
"id": "add:1",
|
"id": "add:1",
|
||||||
"type": "return",
|
"type": "return",
|
||||||
},
|
},
|
||||||
|
@ -61,7 +61,7 @@ export class OpClient<Ops extends OpSchema> extends AutoMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ('error' in msg) {
|
if ('error' in msg) {
|
||||||
pending.reject(msg.error);
|
pending.reject(Object.assign(new Error(), msg.error));
|
||||||
} else {
|
} else {
|
||||||
pending.resolve(msg.data);
|
pending.resolve(msg.data);
|
||||||
}
|
}
|
||||||
@ -86,7 +86,7 @@ export class OpClient<Ops extends OpSchema> extends AutoMessageHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ob.error(msg.error);
|
ob.error(Object.assign(new Error(), msg.error));
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly handleSubscriptionCompleteMessage: MessageHandlers['complete'] =
|
private readonly handleSubscriptionCompleteMessage: MessageHandlers['complete'] =
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import EventEmitter2 from 'eventemitter2';
|
import EventEmitter2 from 'eventemitter2';
|
||||||
|
import { pick } from 'lodash-es';
|
||||||
import { defer, from, fromEvent, Observable, of, take, takeUntil } from 'rxjs';
|
import { defer, from, fromEvent, Observable, of, take, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
import { MANUALLY_STOP } from '../utils';
|
import { MANUALLY_STOP } from '../utils';
|
||||||
@ -70,7 +71,15 @@ export class OpConsumer<Ops extends OpSchema> extends AutoMessageHandler {
|
|||||||
this.port.postMessage({
|
this.port.postMessage({
|
||||||
type: 'return',
|
type: 'return',
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
error: error as Error,
|
error: pick(error, [
|
||||||
|
'name',
|
||||||
|
'message',
|
||||||
|
'code',
|
||||||
|
'type',
|
||||||
|
'status',
|
||||||
|
'data',
|
||||||
|
'stacktrace',
|
||||||
|
]),
|
||||||
} satisfies ReturnMessage);
|
} satisfies ReturnMessage);
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
@ -100,7 +109,15 @@ export class OpConsumer<Ops extends OpSchema> extends AutoMessageHandler {
|
|||||||
this.port.postMessage({
|
this.port.postMessage({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
error: error as Error,
|
error: pick(error, [
|
||||||
|
'name',
|
||||||
|
'message',
|
||||||
|
'code',
|
||||||
|
'type',
|
||||||
|
'status',
|
||||||
|
'data',
|
||||||
|
'stacktrace',
|
||||||
|
]),
|
||||||
} satisfies SubscriptionErrorMessage);
|
} satisfies SubscriptionErrorMessage);
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
import { switchMap } from 'rxjs';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AggregateOptions,
|
AggregateOptions,
|
||||||
IndexerSchema,
|
IndexerSchema,
|
||||||
IndexerStorage,
|
|
||||||
Query,
|
Query,
|
||||||
SearchOptions,
|
SearchOptions,
|
||||||
} from '../storage';
|
} from '../storage';
|
||||||
import type { IndexerSync } from '../sync/indexer';
|
import type { IndexerPreferOptions, IndexerSync } from '../sync/indexer';
|
||||||
import { fromPromise } from '../utils/from-promise';
|
|
||||||
|
|
||||||
export class IndexerFrontend {
|
export class IndexerFrontend {
|
||||||
constructor(
|
constructor(public readonly sync: IndexerSync) {}
|
||||||
public readonly storage: IndexerStorage,
|
|
||||||
public readonly sync: IndexerSync
|
|
||||||
) {}
|
|
||||||
|
|
||||||
get state$() {
|
get state$() {
|
||||||
return this.sync.state$;
|
return this.sync.state$;
|
||||||
@ -27,47 +20,47 @@ export class IndexerFrontend {
|
|||||||
async search<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
async search<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
||||||
table: T,
|
table: T,
|
||||||
query: Query<T>,
|
query: Query<T>,
|
||||||
options?: O
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
) {
|
) {
|
||||||
await this.waitForConnected();
|
return this.sync.search(table, query, options);
|
||||||
return this.storage.search(table, query, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async aggregate<
|
async aggregate<
|
||||||
T extends keyof IndexerSchema,
|
T extends keyof IndexerSchema,
|
||||||
const O extends AggregateOptions<T>,
|
const O extends AggregateOptions<T>,
|
||||||
>(table: T, query: Query<T>, field: keyof IndexerSchema[T], options?: O) {
|
>(
|
||||||
await this.waitForConnected();
|
table: T,
|
||||||
return this.storage.aggregate(table, query, field, options);
|
query: Query<T>,
|
||||||
|
field: keyof IndexerSchema[T],
|
||||||
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
|
) {
|
||||||
|
return this.sync.aggregate(table, query, field, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
search$<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
search$<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
||||||
table: T,
|
table: T,
|
||||||
query: Query<T>,
|
query: Query<T>,
|
||||||
options?: O
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
) {
|
) {
|
||||||
return fromPromise(signal => this.waitForConnected(signal)).pipe(
|
return this.sync.search$(table, query, options);
|
||||||
switchMap(() => this.storage.search$(table, query, options))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
aggregate$<
|
aggregate$<
|
||||||
T extends keyof IndexerSchema,
|
T extends keyof IndexerSchema,
|
||||||
const O extends AggregateOptions<T>,
|
const O extends AggregateOptions<T>,
|
||||||
>(table: T, query: Query<T>, field: keyof IndexerSchema[T], options?: O) {
|
>(
|
||||||
return fromPromise(signal => this.waitForConnected(signal)).pipe(
|
table: T,
|
||||||
switchMap(() => this.storage.aggregate$(table, query, field, options))
|
query: Query<T>,
|
||||||
);
|
field: keyof IndexerSchema[T],
|
||||||
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
|
) {
|
||||||
|
return this.sync.aggregate$(table, query, field, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
addPriority(docId: string, priority: number) {
|
addPriority(docId: string, priority: number) {
|
||||||
return this.sync.addPriority(docId, priority);
|
return this.sync.addPriority(docId, priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
private waitForConnected(signal?: AbortSignal) {
|
|
||||||
return this.storage.connection.waitForConnected(signal);
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForCompleted(signal?: AbortSignal) {
|
waitForCompleted(signal?: AbortSignal) {
|
||||||
return this.sync.waitForCompleted(signal);
|
return this.sync.waitForCompleted(signal);
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,11 @@ import type { PeerStorageOptions } from './types';
|
|||||||
|
|
||||||
export type { BlobSyncState } from './blob';
|
export type { BlobSyncState } from './blob';
|
||||||
export type { DocSyncDocState, DocSyncState } from './doc';
|
export type { DocSyncDocState, DocSyncState } from './doc';
|
||||||
export type { IndexerDocSyncState, IndexerSyncState } from './indexer';
|
export type {
|
||||||
|
IndexerDocSyncState,
|
||||||
|
IndexerPreferOptions,
|
||||||
|
IndexerSyncState,
|
||||||
|
} from './indexer';
|
||||||
|
|
||||||
export interface SyncState {
|
export interface SyncState {
|
||||||
doc?: DocSyncState;
|
doc?: DocSyncState;
|
||||||
@ -65,7 +69,19 @@ export class Sync {
|
|||||||
])
|
])
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
this.indexer = new IndexerSyncImpl(doc, indexer, indexerSync);
|
this.indexer = new IndexerSyncImpl(
|
||||||
|
doc,
|
||||||
|
{
|
||||||
|
local: indexer,
|
||||||
|
remotes: Object.fromEntries(
|
||||||
|
Object.entries(storages.remotes).map(([peerId, remote]) => [
|
||||||
|
peerId,
|
||||||
|
remote.get('indexer'),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
},
|
||||||
|
indexerSync
|
||||||
|
);
|
||||||
|
|
||||||
this.state$ = this.doc.state$.pipe(map(doc => ({ doc })));
|
this.state$ = this.doc.state$.pipe(map(doc => ({ doc })));
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { readAllDocsFromRootDoc } from '@affine/reader';
|
import { readAllDocsFromRootDoc } from '@affine/reader';
|
||||||
|
import { omit } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
filter,
|
filter,
|
||||||
first,
|
first,
|
||||||
@ -7,21 +8,33 @@ import {
|
|||||||
ReplaySubject,
|
ReplaySubject,
|
||||||
share,
|
share,
|
||||||
Subject,
|
Subject,
|
||||||
|
switchMap,
|
||||||
throttleTime,
|
throttleTime,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { applyUpdate, Doc as YDoc } from 'yjs';
|
import { applyUpdate, Doc as YDoc } from 'yjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type AggregateOptions,
|
||||||
|
type AggregateResult,
|
||||||
type DocStorage,
|
type DocStorage,
|
||||||
IndexerDocument,
|
IndexerDocument,
|
||||||
|
type IndexerSchema,
|
||||||
type IndexerStorage,
|
type IndexerStorage,
|
||||||
|
type Query,
|
||||||
|
type SearchOptions,
|
||||||
|
type SearchResult,
|
||||||
} from '../../storage';
|
} from '../../storage';
|
||||||
|
import { DummyIndexerStorage } from '../../storage/dummy/indexer';
|
||||||
import type { IndexerSyncStorage } from '../../storage/indexer-sync';
|
import type { IndexerSyncStorage } from '../../storage/indexer-sync';
|
||||||
import { AsyncPriorityQueue } from '../../utils/async-priority-queue';
|
import { AsyncPriorityQueue } from '../../utils/async-priority-queue';
|
||||||
|
import { fromPromise } from '../../utils/from-promise';
|
||||||
import { takeUntilAbort } from '../../utils/take-until-abort';
|
import { takeUntilAbort } from '../../utils/take-until-abort';
|
||||||
import { MANUALLY_STOP, throwIfAborted } from '../../utils/throw-if-aborted';
|
import { MANUALLY_STOP, throwIfAborted } from '../../utils/throw-if-aborted';
|
||||||
|
import type { PeerStorageOptions } from '../types';
|
||||||
import { crawlingDocData } from './crawler';
|
import { crawlingDocData } from './crawler';
|
||||||
|
|
||||||
|
export type IndexerPreferOptions = 'local' | 'remote';
|
||||||
|
|
||||||
export interface IndexerSyncState {
|
export interface IndexerSyncState {
|
||||||
/**
|
/**
|
||||||
* Number of documents currently in the indexing queue
|
* Number of documents currently in the indexing queue
|
||||||
@ -59,6 +72,35 @@ export interface IndexerSync {
|
|||||||
addPriority(docId: string, priority: number): () => void;
|
addPriority(docId: string, priority: number): () => void;
|
||||||
waitForCompleted(signal?: AbortSignal): Promise<void>;
|
waitForCompleted(signal?: AbortSignal): Promise<void>;
|
||||||
waitForDocCompleted(docId: string, signal?: AbortSignal): Promise<void>;
|
waitForDocCompleted(docId: string, signal?: AbortSignal): Promise<void>;
|
||||||
|
|
||||||
|
search<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
||||||
|
table: T,
|
||||||
|
query: Query<T>,
|
||||||
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
|
): Promise<SearchResult<T, O>>;
|
||||||
|
|
||||||
|
aggregate<T extends keyof IndexerSchema, const O extends AggregateOptions<T>>(
|
||||||
|
table: T,
|
||||||
|
query: Query<T>,
|
||||||
|
field: keyof IndexerSchema[T],
|
||||||
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
|
): Promise<AggregateResult<T, O>>;
|
||||||
|
|
||||||
|
search$<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
||||||
|
table: T,
|
||||||
|
query: Query<T>,
|
||||||
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
|
): Observable<SearchResult<T, O>>;
|
||||||
|
|
||||||
|
aggregate$<
|
||||||
|
T extends keyof IndexerSchema,
|
||||||
|
const O extends AggregateOptions<T>,
|
||||||
|
>(
|
||||||
|
table: T,
|
||||||
|
query: Query<T>,
|
||||||
|
field: keyof IndexerSchema[T],
|
||||||
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
|
): Observable<AggregateResult<T, O>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IndexerSyncImpl implements IndexerSync {
|
export class IndexerSyncImpl implements IndexerSync {
|
||||||
@ -70,6 +112,9 @@ export class IndexerSyncImpl implements IndexerSync {
|
|||||||
private readonly rootDocId = this.doc.spaceId;
|
private readonly rootDocId = this.doc.spaceId;
|
||||||
private readonly status = new IndexerSyncStatus(this.rootDocId);
|
private readonly status = new IndexerSyncStatus(this.rootDocId);
|
||||||
|
|
||||||
|
private readonly indexer: IndexerStorage;
|
||||||
|
private readonly remote?: IndexerStorage;
|
||||||
|
|
||||||
state$ = this.status.state$.pipe(
|
state$ = this.status.state$.pipe(
|
||||||
// throttle the state to 1 second to avoid spamming the UI
|
// throttle the state to 1 second to avoid spamming the UI
|
||||||
throttleTime(1000, undefined, {
|
throttleTime(1000, undefined, {
|
||||||
@ -106,9 +151,13 @@ export class IndexerSyncImpl implements IndexerSync {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly doc: DocStorage,
|
readonly doc: DocStorage,
|
||||||
readonly indexer: IndexerStorage,
|
readonly peers: PeerStorageOptions<IndexerStorage>,
|
||||||
readonly indexerSync: IndexerSyncStorage
|
readonly indexerSync: IndexerSyncStorage
|
||||||
) {}
|
) {
|
||||||
|
// sync feature only works on local indexer
|
||||||
|
this.indexer = this.peers.local;
|
||||||
|
this.remote = Object.values(this.peers.remotes).find(remote => !!remote);
|
||||||
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (this.abort) {
|
if (this.abort) {
|
||||||
@ -439,6 +488,116 @@ export class IndexerSyncImpl implements IndexerSync {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async search<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
||||||
|
table: T,
|
||||||
|
query: Query<T>,
|
||||||
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
|
): Promise<SearchResult<T, O>> {
|
||||||
|
if (
|
||||||
|
options?.prefer === 'remote' &&
|
||||||
|
this.remote &&
|
||||||
|
!(this.remote instanceof DummyIndexerStorage)
|
||||||
|
) {
|
||||||
|
await this.remote.connection.waitForConnected();
|
||||||
|
return await this.remote.search(table, query, omit(options, 'prefer'));
|
||||||
|
} else {
|
||||||
|
await this.indexer.connection.waitForConnected();
|
||||||
|
return await this.indexer.search(table, query, omit(options, 'prefer'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async aggregate<
|
||||||
|
T extends keyof IndexerSchema,
|
||||||
|
const O extends AggregateOptions<T>,
|
||||||
|
>(
|
||||||
|
table: T,
|
||||||
|
query: Query<T>,
|
||||||
|
field: keyof IndexerSchema[T],
|
||||||
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
|
): Promise<AggregateResult<T, O>> {
|
||||||
|
if (
|
||||||
|
options?.prefer === 'remote' &&
|
||||||
|
this.remote &&
|
||||||
|
!(this.remote instanceof DummyIndexerStorage)
|
||||||
|
) {
|
||||||
|
await this.remote.connection.waitForConnected();
|
||||||
|
return await this.remote.aggregate(
|
||||||
|
table,
|
||||||
|
query,
|
||||||
|
field,
|
||||||
|
omit(options, 'prefer')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await this.indexer.connection.waitForConnected();
|
||||||
|
return await this.indexer.aggregate(
|
||||||
|
table,
|
||||||
|
query,
|
||||||
|
field,
|
||||||
|
omit(options, 'prefer')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
search$<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
||||||
|
table: T,
|
||||||
|
query: Query<T>,
|
||||||
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
|
): Observable<SearchResult<T, O>> {
|
||||||
|
if (
|
||||||
|
options?.prefer === 'remote' &&
|
||||||
|
this.remote &&
|
||||||
|
!(this.remote instanceof DummyIndexerStorage)
|
||||||
|
) {
|
||||||
|
const remote = this.remote;
|
||||||
|
return fromPromise(signal =>
|
||||||
|
remote.connection.waitForConnected(signal)
|
||||||
|
).pipe(
|
||||||
|
switchMap(() => remote.search$(table, query, omit(options, 'prefer')))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return fromPromise(signal =>
|
||||||
|
this.indexer.connection.waitForConnected(signal)
|
||||||
|
).pipe(
|
||||||
|
switchMap(() =>
|
||||||
|
this.indexer.search$(table, query, omit(options, 'prefer'))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregate$<
|
||||||
|
T extends keyof IndexerSchema,
|
||||||
|
const O extends AggregateOptions<T>,
|
||||||
|
>(
|
||||||
|
table: T,
|
||||||
|
query: Query<T>,
|
||||||
|
field: keyof IndexerSchema[T],
|
||||||
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
|
): Observable<AggregateResult<T, O>> {
|
||||||
|
if (
|
||||||
|
options?.prefer === 'remote' &&
|
||||||
|
this.remote &&
|
||||||
|
!(this.remote instanceof DummyIndexerStorage)
|
||||||
|
) {
|
||||||
|
const remote = this.remote;
|
||||||
|
return fromPromise(signal =>
|
||||||
|
remote.connection.waitForConnected(signal)
|
||||||
|
).pipe(
|
||||||
|
switchMap(() =>
|
||||||
|
remote.aggregate$(table, query, field, omit(options, 'prefer'))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return fromPromise(signal =>
|
||||||
|
this.indexer.connection.waitForConnected(signal)
|
||||||
|
).pipe(
|
||||||
|
switchMap(() =>
|
||||||
|
this.indexer.aggregate$(table, query, field, omit(options, 'prefer'))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class IndexerSyncStatus {
|
class IndexerSyncStatus {
|
||||||
|
@ -18,9 +18,7 @@ import {
|
|||||||
type DocRecord,
|
type DocRecord,
|
||||||
type DocStorage,
|
type DocStorage,
|
||||||
type DocUpdate,
|
type DocUpdate,
|
||||||
type IndexerDocument,
|
|
||||||
type IndexerSchema,
|
type IndexerSchema,
|
||||||
type IndexerStorage,
|
|
||||||
type ListedBlobRecord,
|
type ListedBlobRecord,
|
||||||
type Query,
|
type Query,
|
||||||
type SearchOptions,
|
type SearchOptions,
|
||||||
@ -29,7 +27,7 @@ import {
|
|||||||
import type { AwarenessSync } from '../sync/awareness';
|
import type { AwarenessSync } from '../sync/awareness';
|
||||||
import type { BlobSync } from '../sync/blob';
|
import type { BlobSync } from '../sync/blob';
|
||||||
import type { DocSync } from '../sync/doc';
|
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';
|
import type { StoreInitOptions, WorkerManagerOps, WorkerOps } from './ops';
|
||||||
|
|
||||||
export type { StoreInitOptions as WorkerInitOptions } 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.docFrontend = new DocFrontend(this.docStorage, this.docSync);
|
||||||
this.blobFrontend = new BlobFrontend(this.blobStorage, this.blobSync);
|
this.blobFrontend = new BlobFrontend(this.blobStorage, this.blobSync);
|
||||||
this.awarenessFrontend = new AwarenessFrontend(this.awarenessSync);
|
this.awarenessFrontend = new AwarenessFrontend(this.awarenessSync);
|
||||||
this.indexerStorage = new WorkerIndexerStorage(this.client);
|
|
||||||
this.indexerSync = new WorkerIndexerSync(this.client);
|
this.indexerSync = new WorkerIndexerSync(this.client);
|
||||||
this.indexerFrontend = new IndexerFrontend(
|
this.indexerFrontend = new IndexerFrontend(this.indexerSync);
|
||||||
this.indexerStorage,
|
|
||||||
this.indexerSync
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly docStorage: WorkerDocStorage;
|
private readonly docStorage: WorkerDocStorage;
|
||||||
@ -113,7 +107,6 @@ export class StoreClient {
|
|||||||
private readonly docSync: WorkerDocSync;
|
private readonly docSync: WorkerDocSync;
|
||||||
private readonly blobSync: WorkerBlobSync;
|
private readonly blobSync: WorkerBlobSync;
|
||||||
private readonly awarenessSync: WorkerAwarenessSync;
|
private readonly awarenessSync: WorkerAwarenessSync;
|
||||||
private readonly indexerStorage: WorkerIndexerStorage;
|
|
||||||
private readonly indexerSync: WorkerIndexerSync;
|
private readonly indexerSync: WorkerIndexerSync;
|
||||||
|
|
||||||
readonly docFrontend: DocFrontend;
|
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>) {}
|
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>>(
|
search<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
||||||
table: T,
|
table: T,
|
||||||
query: Query<T>,
|
query: Query<T>,
|
||||||
options?: O
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
): Promise<SearchResult<T, O>> {
|
): 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>>(
|
aggregate<T extends keyof IndexerSchema, const O extends AggregateOptions<T>>(
|
||||||
table: T,
|
table: T,
|
||||||
query: Query<T>,
|
query: Query<T>,
|
||||||
field: keyof IndexerSchema[T],
|
field: keyof IndexerSchema[T],
|
||||||
options?: O
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
): Promise<AggregateResult<T, O>> {
|
): Promise<AggregateResult<T, O>> {
|
||||||
return this.client.call('indexerStorage.aggregate', {
|
return this.client.call('indexerSync.aggregate', {
|
||||||
table,
|
table,
|
||||||
query,
|
query,
|
||||||
field: field as string,
|
field: field as string,
|
||||||
@ -377,9 +367,9 @@ class WorkerIndexerStorage implements IndexerStorage {
|
|||||||
search$<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
search$<T extends keyof IndexerSchema, const O extends SearchOptions<T>>(
|
||||||
table: T,
|
table: T,
|
||||||
query: Query<T>,
|
query: Query<T>,
|
||||||
options?: O
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
): Observable<SearchResult<T, O>> {
|
): Observable<SearchResult<T, O>> {
|
||||||
return this.client.ob$('indexerStorage.subscribeSearch', {
|
return this.client.ob$('indexerSync.subscribeSearch', {
|
||||||
table,
|
table,
|
||||||
query,
|
query,
|
||||||
options,
|
options,
|
||||||
@ -392,59 +382,16 @@ class WorkerIndexerStorage implements IndexerStorage {
|
|||||||
table: T,
|
table: T,
|
||||||
query: Query<T>,
|
query: Query<T>,
|
||||||
field: keyof IndexerSchema[T],
|
field: keyof IndexerSchema[T],
|
||||||
options?: O
|
options?: O & { prefer?: IndexerPreferOptions }
|
||||||
): Observable<AggregateResult<T, O>> {
|
): Observable<AggregateResult<T, O>> {
|
||||||
return this.client.ob$('indexerStorage.subscribeAggregate', {
|
return this.client.ob$('indexerSync.subscribeAggregate', {
|
||||||
table,
|
table,
|
||||||
query,
|
query,
|
||||||
field: field as string,
|
field: field as string,
|
||||||
options,
|
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> {
|
waitForCompleted(signal?: AbortSignal): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const abortListener = () => {
|
const abortListener = () => {
|
||||||
|
@ -265,16 +265,6 @@ class StoreConsumer {
|
|||||||
}),
|
}),
|
||||||
'awarenessSync.collect': ({ collectId, awareness }) =>
|
'awarenessSync.collect': ({ collectId, awareness }) =>
|
||||||
collectJobs.get(collectId)?.(awareness),
|
collectJobs.get(collectId)?.(awareness),
|
||||||
'indexerStorage.aggregate': ({ table, query, field, options }) =>
|
|
||||||
this.indexerStorage.aggregate(table, query, field, options),
|
|
||||||
'indexerStorage.search': ({ table, query, options }) =>
|
|
||||||
this.indexerStorage.search(table, query, options),
|
|
||||||
'indexerStorage.subscribeSearch': ({ table, query, options }) =>
|
|
||||||
this.indexerStorage.search$(table, query, options),
|
|
||||||
'indexerStorage.subscribeAggregate': ({ table, query, field, options }) =>
|
|
||||||
this.indexerStorage.aggregate$(table, query, field, options),
|
|
||||||
'indexerStorage.waitForConnected': (_, ctx) =>
|
|
||||||
this.indexerStorage.connection.waitForConnected(ctx.signal),
|
|
||||||
'indexerSync.state': () => this.indexerSync.state$,
|
'indexerSync.state': () => this.indexerSync.state$,
|
||||||
'indexerSync.docState': (docId: string) =>
|
'indexerSync.docState': (docId: string) =>
|
||||||
this.indexerSync.docState$(docId),
|
this.indexerSync.docState$(docId),
|
||||||
@ -287,6 +277,14 @@ class StoreConsumer {
|
|||||||
this.indexerSync.waitForCompleted(ctx.signal),
|
this.indexerSync.waitForCompleted(ctx.signal),
|
||||||
'indexerSync.waitForDocCompleted': (docId: string, ctx) =>
|
'indexerSync.waitForDocCompleted': (docId: string, ctx) =>
|
||||||
this.indexerSync.waitForDocCompleted(docId, ctx.signal),
|
this.indexerSync.waitForDocCompleted(docId, ctx.signal),
|
||||||
|
'indexerSync.aggregate': ({ table, query, field, options }) =>
|
||||||
|
this.indexerSync.aggregate(table, query, field, options),
|
||||||
|
'indexerSync.search': ({ table, query, options }) =>
|
||||||
|
this.indexerSync.search(table, query, options),
|
||||||
|
'indexerSync.subscribeSearch': ({ table, query, options }) =>
|
||||||
|
this.indexerSync.search$(table, query, options),
|
||||||
|
'indexerSync.subscribeAggregate': ({ table, query, field, options }) =>
|
||||||
|
this.indexerSync.aggregate$(table, query, field, options),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import type { AvailableStorageImplementations } from '../impls';
|
import type { AvailableStorageImplementations } from '../impls';
|
||||||
import type {
|
import type {
|
||||||
AggregateOptions,
|
|
||||||
AggregateResult,
|
AggregateResult,
|
||||||
BlobRecord,
|
BlobRecord,
|
||||||
DocClock,
|
DocClock,
|
||||||
@ -10,7 +9,6 @@ import type {
|
|||||||
DocUpdate,
|
DocUpdate,
|
||||||
ListedBlobRecord,
|
ListedBlobRecord,
|
||||||
Query,
|
Query,
|
||||||
SearchOptions,
|
|
||||||
SearchResult,
|
SearchResult,
|
||||||
StorageType,
|
StorageType,
|
||||||
} from '../storage';
|
} from '../storage';
|
||||||
@ -69,36 +67,6 @@ interface GroupedWorkerOps {
|
|||||||
waitForConnected: [void, void];
|
waitForConnected: [void, void];
|
||||||
};
|
};
|
||||||
|
|
||||||
indexerStorage: {
|
|
||||||
search: [
|
|
||||||
{ table: string; query: Query<any>; options?: SearchOptions<any> },
|
|
||||||
SearchResult<any, any>,
|
|
||||||
];
|
|
||||||
aggregate: [
|
|
||||||
{
|
|
||||||
table: string;
|
|
||||||
query: Query<any>;
|
|
||||||
field: string;
|
|
||||||
options?: AggregateOptions<any>;
|
|
||||||
},
|
|
||||||
AggregateResult<any, any>,
|
|
||||||
];
|
|
||||||
subscribeSearch: [
|
|
||||||
{ table: string; query: Query<any>; options?: SearchOptions<any> },
|
|
||||||
SearchResult<any, any>,
|
|
||||||
];
|
|
||||||
subscribeAggregate: [
|
|
||||||
{
|
|
||||||
table: string;
|
|
||||||
query: Query<any>;
|
|
||||||
field: string;
|
|
||||||
options?: AggregateOptions<any>;
|
|
||||||
},
|
|
||||||
AggregateResult<any, any>,
|
|
||||||
];
|
|
||||||
waitForConnected: [void, void];
|
|
||||||
};
|
|
||||||
|
|
||||||
docSync: {
|
docSync: {
|
||||||
state: [void, DocSyncState];
|
state: [void, DocSyncState];
|
||||||
docState: [string, DocSyncDocState];
|
docState: [string, DocSyncDocState];
|
||||||
@ -137,6 +105,40 @@ interface GroupedWorkerOps {
|
|||||||
addPriority: [{ docId: string; priority: number }, boolean];
|
addPriority: [{ docId: string; priority: number }, boolean];
|
||||||
waitForCompleted: [void, void];
|
waitForCompleted: [void, void];
|
||||||
waitForDocCompleted: [string, void];
|
waitForDocCompleted: [string, void];
|
||||||
|
search: [
|
||||||
|
{
|
||||||
|
table: string;
|
||||||
|
query: Query<any>;
|
||||||
|
options?: any;
|
||||||
|
},
|
||||||
|
SearchResult<any, any>,
|
||||||
|
];
|
||||||
|
aggregate: [
|
||||||
|
{
|
||||||
|
table: string;
|
||||||
|
query: Query<any>;
|
||||||
|
field: string;
|
||||||
|
options?: any;
|
||||||
|
},
|
||||||
|
AggregateResult<any, any>,
|
||||||
|
];
|
||||||
|
subscribeSearch: [
|
||||||
|
{
|
||||||
|
table: string;
|
||||||
|
query: Query<any>;
|
||||||
|
options?: any;
|
||||||
|
},
|
||||||
|
SearchResult<any, any>,
|
||||||
|
];
|
||||||
|
subscribeAggregate: [
|
||||||
|
{
|
||||||
|
table: string;
|
||||||
|
query: Query<any>;
|
||||||
|
field: string;
|
||||||
|
options?: any;
|
||||||
|
},
|
||||||
|
AggregateResult<any, any>,
|
||||||
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +112,7 @@ export class DocsSearchService extends Service {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
prefer: 'remote',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@ -3,7 +3,6 @@ import type { FlagInfo } from './types';
|
|||||||
// const isNotStableBuild = BUILD_CONFIG.appBuildType !== 'stable';
|
// const isNotStableBuild = BUILD_CONFIG.appBuildType !== 'stable';
|
||||||
const isDesktopEnvironment = BUILD_CONFIG.isElectron;
|
const isDesktopEnvironment = BUILD_CONFIG.isElectron;
|
||||||
const isCanaryBuild = BUILD_CONFIG.appBuildType === 'canary';
|
const isCanaryBuild = BUILD_CONFIG.appBuildType === 'canary';
|
||||||
const isBetaBuild = BUILD_CONFIG.appBuildType === 'beta';
|
|
||||||
const isMobile = BUILD_CONFIG.isMobileEdition;
|
const isMobile = BUILD_CONFIG.isMobileEdition;
|
||||||
|
|
||||||
export const AFFINE_FLAGS = {
|
export const AFFINE_FLAGS = {
|
||||||
@ -266,13 +265,6 @@ export const AFFINE_FLAGS = {
|
|||||||
configurable: isCanaryBuild,
|
configurable: isCanaryBuild,
|
||||||
defaultState: false,
|
defaultState: false,
|
||||||
},
|
},
|
||||||
enable_cloud_indexer: {
|
|
||||||
category: 'affine',
|
|
||||||
displayName: 'Enable Cloud Indexer',
|
|
||||||
description: 'Use cloud indexer to search docs',
|
|
||||||
configurable: isBetaBuild || isCanaryBuild,
|
|
||||||
defaultState: false,
|
|
||||||
},
|
|
||||||
enable_adapter_panel: {
|
enable_adapter_panel: {
|
||||||
category: 'affine',
|
category: 'affine',
|
||||||
displayName:
|
displayName:
|
||||||
|
@ -25,6 +25,11 @@ export class QuickSearch extends Entity {
|
|||||||
.flat()
|
.flat()
|
||||||
.map(items => items.flat());
|
.map(items => items.flat());
|
||||||
|
|
||||||
|
readonly error$ = this.state$
|
||||||
|
.map(s => s?.sessions.map(session => session.error$) ?? [])
|
||||||
|
.flat()
|
||||||
|
.map(items => items.find(v => !!v) ?? null);
|
||||||
|
|
||||||
readonly show$ = this.state$.map(s => !!s);
|
readonly show$ = this.state$.map(s => !!s);
|
||||||
|
|
||||||
readonly options$ = this.state$.map(s => s?.options);
|
readonly options$ = this.state$.map(s => s?.options);
|
||||||
|
@ -6,11 +6,12 @@ import {
|
|||||||
onStart,
|
onStart,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { truncate } from 'lodash-es';
|
import { truncate } from 'lodash-es';
|
||||||
import { map, of, switchMap, tap, throttleTime } from 'rxjs';
|
import { catchError, EMPTY, map, of, switchMap, tap, throttleTime } from 'rxjs';
|
||||||
|
|
||||||
import type { DocRecord, DocsService } from '../../doc';
|
import type { DocRecord, DocsService } from '../../doc';
|
||||||
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
||||||
import type { DocsSearchService } from '../../docs-search';
|
import type { DocsSearchService } from '../../docs-search';
|
||||||
|
import type { WorkspaceService } from '../../workspace';
|
||||||
import type { QuickSearchSession } from '../providers/quick-search-provider';
|
import type { QuickSearchSession } from '../providers/quick-search-provider';
|
||||||
import type { QuickSearchItem } from '../types/item';
|
import type { QuickSearchItem } from '../types/item';
|
||||||
|
|
||||||
@ -26,6 +27,7 @@ export class DocsQuickSearchSession
|
|||||||
implements QuickSearchSession<'docs', DocsPayload>
|
implements QuickSearchSession<'docs', DocsPayload>
|
||||||
{
|
{
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly workspaceService: WorkspaceService,
|
||||||
private readonly docsSearchService: DocsSearchService,
|
private readonly docsSearchService: DocsSearchService,
|
||||||
private readonly docsService: DocsService,
|
private readonly docsService: DocsService,
|
||||||
private readonly docDisplayMetaService: DocDisplayMetaService
|
private readonly docDisplayMetaService: DocDisplayMetaService
|
||||||
@ -41,10 +43,17 @@ export class DocsQuickSearchSession
|
|||||||
|
|
||||||
private readonly isQueryLoading$ = new LiveData(false);
|
private readonly isQueryLoading$ = new LiveData(false);
|
||||||
|
|
||||||
|
isCloudWorkspace = this.workspaceService.workspace.flavour !== 'local';
|
||||||
|
|
||||||
isLoading$ = LiveData.computed(get => {
|
isLoading$ = LiveData.computed(get => {
|
||||||
return get(this.isIndexerLoading$) || get(this.isQueryLoading$);
|
return (
|
||||||
|
(this.isCloudWorkspace ? false : get(this.isIndexerLoading$)) ||
|
||||||
|
get(this.isQueryLoading$)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
error$ = new LiveData<any>(null);
|
||||||
|
|
||||||
query$ = new LiveData('');
|
query$ = new LiveData('');
|
||||||
|
|
||||||
items$ = new LiveData<QuickSearchItem<'docs', DocsPayload>[]>([]);
|
items$ = new LiveData<QuickSearchItem<'docs', DocsPayload>[]>([]);
|
||||||
@ -102,9 +111,16 @@ export class DocsQuickSearchSession
|
|||||||
this.isQueryLoading$.next(false);
|
this.isQueryLoading$.next(false);
|
||||||
}),
|
}),
|
||||||
onStart(() => {
|
onStart(() => {
|
||||||
|
this.error$.next(null);
|
||||||
this.items$.next([]);
|
this.items$.next([]);
|
||||||
this.isQueryLoading$.next(true);
|
this.isQueryLoading$.next(true);
|
||||||
}),
|
}),
|
||||||
|
catchError(err => {
|
||||||
|
this.error$.next(err instanceof Error ? err.message : err);
|
||||||
|
this.items$.next([]);
|
||||||
|
this.isQueryLoading$.next(false);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
onComplete(() => {})
|
onComplete(() => {})
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -55,6 +55,7 @@ export function configureQuickSearchModule(framework: Framework) {
|
|||||||
.entity(QuickSearch)
|
.entity(QuickSearch)
|
||||||
.entity(CommandsQuickSearchSession, [GlobalContextService])
|
.entity(CommandsQuickSearchSession, [GlobalContextService])
|
||||||
.entity(DocsQuickSearchSession, [
|
.entity(DocsQuickSearchSession, [
|
||||||
|
WorkspaceService,
|
||||||
DocsSearchService,
|
DocsSearchService,
|
||||||
DocsService,
|
DocsService,
|
||||||
DocDisplayMetaService,
|
DocDisplayMetaService,
|
||||||
|
@ -8,7 +8,7 @@ export type QuickSearchFunction<S, P> = (
|
|||||||
|
|
||||||
export interface QuickSearchSession<S, P> {
|
export interface QuickSearchSession<S, P> {
|
||||||
items$: LiveData<QuickSearchItem<S, P>[]>;
|
items$: LiveData<QuickSearchItem<S, P>[]>;
|
||||||
isError$?: LiveData<boolean>;
|
error$?: LiveData<any>;
|
||||||
isLoading$?: LiveData<boolean>;
|
isLoading$?: LiveData<boolean>;
|
||||||
loadingProgress$?: LiveData<number>;
|
loadingProgress$?: LiveData<number>;
|
||||||
hasMore$?: LiveData<boolean>;
|
hasMore$?: LiveData<boolean>;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { globalStyle, style } from '@vanilla-extract/css';
|
import { globalStyle, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const root = style({});
|
export const root = style({});
|
||||||
@ -195,3 +196,9 @@ export const itemSubtitle = style({
|
|||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
textAlign: 'justify',
|
textAlign: 'justify',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const errorMessage = style({
|
||||||
|
padding: '0px 8px 8px',
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
color: cssVarV2('status/error'),
|
||||||
|
});
|
||||||
|
@ -24,6 +24,7 @@ export const CMDK = ({
|
|||||||
className,
|
className,
|
||||||
query,
|
query,
|
||||||
groups: newGroups = [],
|
groups: newGroups = [],
|
||||||
|
error,
|
||||||
inputLabel,
|
inputLabel,
|
||||||
placeholder,
|
placeholder,
|
||||||
loading: newLoading = false,
|
loading: newLoading = false,
|
||||||
@ -33,6 +34,7 @@ export const CMDK = ({
|
|||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
className?: string;
|
className?: string;
|
||||||
query: string;
|
query: string;
|
||||||
|
error?: ReactNode;
|
||||||
inputLabel?: ReactNode;
|
inputLabel?: ReactNode;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
@ -200,6 +202,7 @@ export const CMDK = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Command.List ref={listRef} data-opening={opening ? true : undefined}>
|
<Command.List ref={listRef} data-opening={opening ? true : undefined}>
|
||||||
|
{error && <p className={styles.errorMessage}>{error}</p>}
|
||||||
{groups.map(({ group, items }) => {
|
{groups.map(({ group, items }) => {
|
||||||
return (
|
return (
|
||||||
<CMDKGroup
|
<CMDKGroup
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { UserFriendlyError } from '@affine/error';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useServices } from '@toeverything/infra';
|
import { useLiveData, useServices } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
@ -18,6 +19,7 @@ export const QuickSearchContainer = () => {
|
|||||||
const loading = useLiveData(quickSearch.isLoading$);
|
const loading = useLiveData(quickSearch.isLoading$);
|
||||||
const loadingProgress = useLiveData(quickSearch.loadingProgress$);
|
const loadingProgress = useLiveData(quickSearch.loadingProgress$);
|
||||||
const items = useLiveData(quickSearch.items$);
|
const items = useLiveData(quickSearch.items$);
|
||||||
|
const error = useLiveData(quickSearch.error$);
|
||||||
const options = useLiveData(quickSearch.options$);
|
const options = useLiveData(quickSearch.options$);
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
@ -79,6 +81,7 @@ export const QuickSearchContainer = () => {
|
|||||||
<CMDK
|
<CMDK
|
||||||
query={query}
|
query={query}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
|
error={error ? UserFriendlyError.fromAny(error).message : null}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
loadingProgress={loadingProgress}
|
loadingProgress={loadingProgress}
|
||||||
onQueryChange={handleChangeQuery}
|
onQueryChange={handleChangeQuery}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { TagMeta } from '@affine/core/components/page-list';
|
import type { TagMeta } from '@affine/core/components/page-list';
|
||||||
|
import { UserFriendlyError } from '@affine/error';
|
||||||
import { I18n } from '@affine/i18n';
|
import { I18n } from '@affine/i18n';
|
||||||
import { createSignalFromObservable } from '@blocksuite/affine/shared/utils';
|
import { createSignalFromObservable } from '@blocksuite/affine/shared/utils';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
import type { DocMeta } from '@blocksuite/affine/store';
|
||||||
@ -6,14 +7,14 @@ import type {
|
|||||||
LinkedMenuGroup,
|
LinkedMenuGroup,
|
||||||
LinkedMenuItem,
|
LinkedMenuItem,
|
||||||
} from '@blocksuite/affine/widgets/linked-doc';
|
} from '@blocksuite/affine/widgets/linked-doc';
|
||||||
import { CollectionsIcon } from '@blocksuite/icons/lit';
|
import { CollectionsIcon, WarningIcon } from '@blocksuite/icons/lit';
|
||||||
import { computed } from '@preact/signals-core';
|
import { computed, signal } from '@preact/signals-core';
|
||||||
import { Service } from '@toeverything/infra';
|
import { Service } from '@toeverything/infra';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import Fuse, { type FuseResultMatch } from 'fuse.js';
|
import Fuse, { type FuseResultMatch } from 'fuse.js';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||||
import { map } from 'rxjs';
|
import { catchError, map, of } from 'rxjs';
|
||||||
|
|
||||||
import type { CollectionMeta, CollectionService } from '../../collection';
|
import type { CollectionMeta, CollectionService } from '../../collection';
|
||||||
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
||||||
@ -87,6 +88,7 @@ export class SearchMenuService extends Service {
|
|||||||
): LinkedMenuGroup {
|
): LinkedMenuGroup {
|
||||||
const currentWorkspace = this.workspaceService.workspace;
|
const currentWorkspace = this.workspaceService.workspace;
|
||||||
const rawMetas = currentWorkspace.docCollection.meta.docMetas;
|
const rawMetas = currentWorkspace.docCollection.meta.docMetas;
|
||||||
|
const loading = signal(true);
|
||||||
const { signal: docsSignal, cleanup: cleanupDocs } =
|
const { signal: docsSignal, cleanup: cleanupDocs } =
|
||||||
createSignalFromObservable(
|
createSignalFromObservable(
|
||||||
this.searchDocs$(query).pipe(
|
this.searchDocs$(query).pipe(
|
||||||
@ -108,7 +110,26 @@ export class SearchMenuService extends Service {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.filter(m => !!m);
|
.filter(m => !!m);
|
||||||
|
loading.value = false;
|
||||||
return docs;
|
return docs;
|
||||||
|
}),
|
||||||
|
catchError(err => {
|
||||||
|
loading.value = false;
|
||||||
|
const userFriendlyError = UserFriendlyError.fromAny(err);
|
||||||
|
return of([
|
||||||
|
{
|
||||||
|
name: html`<span style="color: ${cssVarV2('status/error')}"
|
||||||
|
>${I18n.t(
|
||||||
|
`error.${userFriendlyError.name}`,
|
||||||
|
userFriendlyError.data
|
||||||
|
)}</span
|
||||||
|
>`,
|
||||||
|
key: 'error',
|
||||||
|
icon: WarningIcon(),
|
||||||
|
disabled: true,
|
||||||
|
action: () => {},
|
||||||
|
},
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
@ -129,6 +150,7 @@ export class SearchMenuService extends Service {
|
|||||||
name: I18n.t('com.affine.editor.at-menu.link-to-doc', {
|
name: I18n.t('com.affine.editor.at-menu.link-to-doc', {
|
||||||
query,
|
query,
|
||||||
}),
|
}),
|
||||||
|
loading: loading,
|
||||||
items: docsSignal,
|
items: docsSignal,
|
||||||
maxDisplay: MAX_DOCS,
|
maxDisplay: MAX_DOCS,
|
||||||
overflowText,
|
overflowText,
|
||||||
@ -170,7 +192,7 @@ export class SearchMenuService extends Service {
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
highlights: node.highlights.title[0],
|
highlights: node.highlights?.title?.[0],
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
|
||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import {
|
import {
|
||||||
createWorkspaceMutation,
|
createWorkspaceMutation,
|
||||||
@ -7,6 +6,7 @@ import {
|
|||||||
getWorkspacesQuery,
|
getWorkspacesQuery,
|
||||||
Permission,
|
Permission,
|
||||||
ServerDeploymentType,
|
ServerDeploymentType,
|
||||||
|
ServerFeature,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import type {
|
import type {
|
||||||
BlobStorage,
|
BlobStorage,
|
||||||
@ -86,7 +86,6 @@ const logger = new DebugLogger('affine:cloud-workspace-flavour-provider');
|
|||||||
class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
||||||
private readonly authService: AuthService;
|
private readonly authService: AuthService;
|
||||||
private readonly graphqlService: GraphQLService;
|
private readonly graphqlService: GraphQLService;
|
||||||
private readonly featureFlagService: FeatureFlagService;
|
|
||||||
private readonly unsubscribeAccountChanged: () => void;
|
private readonly unsubscribeAccountChanged: () => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -95,7 +94,6 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
|||||||
) {
|
) {
|
||||||
this.authService = server.scope.get(AuthService);
|
this.authService = server.scope.get(AuthService);
|
||||||
this.graphqlService = server.scope.get(GraphQLService);
|
this.graphqlService = server.scope.get(GraphQLService);
|
||||||
this.featureFlagService = server.scope.get(FeatureFlagService);
|
|
||||||
this.unsubscribeAccountChanged = this.server.scope.eventBus.on(
|
this.unsubscribeAccountChanged = this.server.scope.eventBus.on(
|
||||||
AccountChanged,
|
AccountChanged,
|
||||||
() => {
|
() => {
|
||||||
@ -477,24 +475,14 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
|||||||
id: `${this.flavour}:${workspaceId}`,
|
id: `${this.flavour}:${workspaceId}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
indexer: this.featureFlagService.flags.enable_cloud_indexer.value
|
indexer: {
|
||||||
? {
|
name: 'IndexedDBIndexerStorage',
|
||||||
name: 'CloudIndexerStorage',
|
opts: {
|
||||||
opts: {
|
flavour: this.flavour,
|
||||||
flavour: this.flavour,
|
type: 'workspace',
|
||||||
type: 'workspace',
|
id: workspaceId,
|
||||||
id: workspaceId,
|
},
|
||||||
serverBaseUrl: this.server.serverMetadata.baseUrl,
|
},
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
name: 'IndexedDBIndexerStorage',
|
|
||||||
opts: {
|
|
||||||
flavour: this.flavour,
|
|
||||||
type: 'workspace',
|
|
||||||
id: workspaceId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
indexerSync: {
|
indexerSync: {
|
||||||
name: 'IndexedDBIndexerSyncStorage',
|
name: 'IndexedDBIndexerSyncStorage',
|
||||||
opts: {
|
opts: {
|
||||||
@ -535,6 +523,19 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
|||||||
ServerDeploymentType.Selfhosted,
|
ServerDeploymentType.Selfhosted,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
indexer: this.server.config$.value.features.includes(
|
||||||
|
ServerFeature.Indexer
|
||||||
|
)
|
||||||
|
? {
|
||||||
|
name: 'CloudIndexerStorage',
|
||||||
|
opts: {
|
||||||
|
flavour: this.flavour,
|
||||||
|
type: 'workspace',
|
||||||
|
id: workspaceId,
|
||||||
|
serverBaseUrl: this.server.serverMetadata.baseUrl,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
v1: {
|
v1: {
|
||||||
doc: this.DocStorageV1Type
|
doc: this.DocStorageV1Type
|
||||||
|
@ -308,5 +308,7 @@ export async function enableCloudWorkspaceFromShareButton(page: Page) {
|
|||||||
export async function enableShare(page: Page) {
|
export async function enableShare(page: Page) {
|
||||||
await page.getByTestId('cloud-share-menu-button').click();
|
await page.getByTestId('cloud-share-menu-button').click();
|
||||||
await page.getByTestId('share-link-menu-trigger').click();
|
await page.getByTestId('share-link-menu-trigger').click();
|
||||||
|
// wait for the menu to be visible
|
||||||
|
await page.waitForTimeout(500);
|
||||||
await page.getByTestId('share-link-menu-enable-share').click();
|
await page.getByTestId('share-link-menu-enable-share').click();
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user