From 6c6ce08a39a752950d903035f700cdb9563a2276 Mon Sep 17 00:00:00 2001 From: Hypatia May Date: Sat, 11 Jul 2026 10:52:17 +0800 Subject: [PATCH] fix(session-query): preserve sync error typing --- .../session-query/src/provider.ts | 28 +++++++++++++------ .../session-query/tests/session-query.spec.ts | 24 ++++++++++++++++ 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/packages/session-query/session-query/src/provider.ts b/packages/session-query/session-query/src/provider.ts index d6190701a7..a8b6e3038c 100644 --- a/packages/session-query/session-query/src/provider.ts +++ b/packages/session-query/session-query/src/provider.ts @@ -166,12 +166,18 @@ export class SessionProviderCoordinator { private _syncAll(state: ProviderState): Promise { // Capture the direct source before awaiting: only searches that observed // the same live corpus may share an in-flight full synchronization. - const liveSessions = this._corpus().listLive() - const liveKey = JSON.stringify(liveSessions.map(session => this._snapshotLive(session)).map(snapshot => [ - snapshot.session.header.id, - snapshot.fingerprint, - snapshot.session.persisted, - ])) + let liveSessions: Session[] + let liveKey: string + try { + liveSessions = this._corpus().listLive() + liveKey = JSON.stringify(liveSessions.map(session => this._snapshotLive(session)).map(snapshot => [ + snapshot.session.header.id, + snapshot.fingerprint, + snapshot.session.persisted, + ])) + } catch (error: unknown) { + return Promise.reject(this._synchronizationError(state, error)) + } if (state.fullSync?.liveKey === liveKey) return state.fullSync.promise const promise = this._enqueue(state, async () => { /* v8 ignore next -- a provider can be disposed while queued behind an in-flight update */ @@ -242,12 +248,16 @@ export class SessionProviderCoordinator { const next = state.chain.then(operation, operation) state.chain = next.then(() => undefined, () => undefined) return next.catch((error: unknown) => { - /* v8 ignore next -- service-created typed synchronization errors pass through unchanged */ - if (error instanceof SessionQueryError) throw error - throw new SessionQueryError(`session-query provider "${state.provider.id}" synchronization failed: ${errorMessage(error)}`, 'SESSION_QUERY_INDEX_FAILED', { cause: error }) + throw this._synchronizationError(state, error) }) } + private _synchronizationError(state: ProviderState, error: unknown): SessionQueryError { + /* v8 ignore next -- service-created typed synchronization errors pass through unchanged */ + if (error instanceof SessionQueryError) return error + return new SessionQueryError(`session-query provider "${state.provider.id}" synchronization failed: ${errorMessage(error)}`, 'SESSION_QUERY_INDEX_FAILED', { cause: error }) + } + private _resolveProvider(): ProviderState { if (this._configuredProviderId !== undefined) { const state = this._providers.get(this._configuredProviderId) diff --git a/packages/session-query/session-query/tests/session-query.spec.ts b/packages/session-query/session-query/tests/session-query.spec.ts index 5daa085bb3..6d7e9cab33 100644 --- a/packages/session-query/session-query/tests/session-query.spec.ts +++ b/packages/session-query/session-query/tests/session-query.spec.ts @@ -673,6 +673,30 @@ describe('provider selection and synchronization', () => { await expect(ctx.sessionQuery.searchSessions({ query: 'x' })).resolves.toMatchObject({ providerId: provider.id }) expect(provider.persisted.get(persisted.id)?.documents[0]?.text).toBe('retry') }) + + it('types synchronous extractor failures during full-search key construction', async () => { + const ctx = await liveContext() + const session = ctx.sessions.create(SessionId('throwing-extractor')) + session.append('test/note', { note: 'unreachable' }) + const provider = new FakeProvider() + ctx.sessionQuery.registerSearchProvider(provider) + const cause = new Error('custom extractor failed') + ctx.sessionQuery.registerEventTextExtractor('test/note', { + version: 'throwing-v1', + extract: () => { throw cause }, + }) + + let thrown: unknown + try { + await ctx.sessionQuery.searchSessions({ query: 'x' }) + } catch (error: unknown) { + thrown = error + } + expect(thrown).toBeInstanceOf(SessionQueryError) + expect(thrown).toMatchObject({ code: 'SESSION_QUERY_INDEX_FAILED', cause }) + expect(asError(thrown).message).toContain(`provider "${provider.id}"`) + expect(provider.sessionRequests).toEqual([]) + }) }) describe('semantic text extractors', () => {