fix(session-query): fail mount when index open fails
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { createHash, randomUUID } from 'node:crypto'
|
import { createHash, randomUUID } from 'node:crypto'
|
||||||
import { DatabaseSync } from 'node:sqlite'
|
import { DatabaseSync } from 'node:sqlite'
|
||||||
import { Context, type Fiber } from 'cordis'
|
import { Context, Service, type Fiber } from 'cordis'
|
||||||
import z from 'schemastery'
|
import z from 'schemastery'
|
||||||
import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
|
import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
|
||||||
import type SessionPersistence from '@deepseek-ai/dsh-session-persistence'
|
import type SessionPersistence from '@deepseek-ai/dsh-session-persistence'
|
||||||
@@ -193,9 +193,6 @@ export class SessionQuerySqlite extends SessionQueryService {
|
|||||||
super(ctx, config)
|
super(ctx, config)
|
||||||
this.config = resolveConfig(config)
|
this.config = resolveConfig(config)
|
||||||
this._ready = this._open()
|
this._ready = this._open()
|
||||||
// Attach a rejection observer immediately; callers still receive the same
|
|
||||||
// rejection from `_ready`, including when no search is ever attempted.
|
|
||||||
void this._ready.catch(() => undefined)
|
|
||||||
this._optionalPersistenceFiber = ctx.inject(['sessionPersistence'], (childCtx: Context) => {
|
this._optionalPersistenceFiber = ctx.inject(['sessionPersistence'], (childCtx: Context) => {
|
||||||
const service = childCtx.sessionPersistence
|
const service = childCtx.sessionPersistence
|
||||||
const binding = { identity: Symbol(), service }
|
const binding = { identity: Symbol(), service }
|
||||||
@@ -212,6 +209,11 @@ export class SessionQuerySqlite extends SessionQueryService {
|
|||||||
ctx.effect(() => async () => this.close(), 'sessionQuerySqlite.close')
|
ctx.effect(() => async () => this.close(), 'sessionQuerySqlite.close')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Open the index before Cordis publishes this combined service as active. */
|
||||||
|
protected async [Service.init](): Promise<void> {
|
||||||
|
await this._ensureReady(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
override async searchSessions(
|
override async searchSessions(
|
||||||
request: SessionSearchRequest,
|
request: SessionSearchRequest,
|
||||||
exec?: SessionSearchExecContext,
|
exec?: SessionSearchExecContext,
|
||||||
|
|||||||
@@ -966,13 +966,14 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
|||||||
|
|
||||||
it('surfaces filesystem failures while pre-creating the database', async () => {
|
it('surfaces filesystem failures while pre-creating the database', async () => {
|
||||||
const path = `${await temporaryPath()}\0`
|
const path = `${await temporaryPath()}\0`
|
||||||
const ctx = await liveContext({ path })
|
const ctx = new Context()
|
||||||
|
await ctx.plugin(SessionStore)
|
||||||
|
|
||||||
await expect(ctx.sessionQuery.searchSessions({ query: 'needle' })).rejects.toMatchObject({
|
await expect(ctx.plugin(SessionQuerySqlite, { path })).rejects.toMatchObject({
|
||||||
code: 'SESSION_QUERY_INDEX_FAILED',
|
code: 'SESSION_QUERY_INDEX_FAILED',
|
||||||
cause: { code: 'ERR_INVALID_ARG_VALUE' },
|
cause: { code: 'ERR_INVALID_ARG_VALUE' },
|
||||||
})
|
})
|
||||||
await (ctx.sessionQuery as SessionQuerySqlite).close()
|
expect(ctx.sessionQuery).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('resets a recognized incompatible derived schema but refuses a foreign database', async () => {
|
it('resets a recognized incompatible derived schema but refuses a foreign database', async () => {
|
||||||
@@ -998,26 +999,28 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
|||||||
foreign.exec('CREATE TABLE canonical(value TEXT)')
|
foreign.exec('CREATE TABLE canonical(value TEXT)')
|
||||||
foreign.exec("INSERT INTO canonical VALUES ('safe')")
|
foreign.exec("INSERT INTO canonical VALUES ('safe')")
|
||||||
foreign.close()
|
foreign.close()
|
||||||
const foreignCtx = await liveContext({ path: foreignPath, journalMode: 'delete' })
|
const foreignCtx = new Context()
|
||||||
await expect(foreignCtx.sessionQuery.searchSessions({ query: 'needle' }))
|
await foreignCtx.plugin(SessionStore)
|
||||||
|
await expect(foreignCtx.plugin(SessionQuerySqlite, { path: foreignPath, journalMode: 'delete' }))
|
||||||
.rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
|
.rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
|
||||||
|
expect(foreignCtx.sessionQuery).toBeUndefined()
|
||||||
const stillForeign = new DatabaseSync(foreignPath)
|
const stillForeign = new DatabaseSync(foreignPath)
|
||||||
expect(stillForeign.prepare('SELECT value FROM canonical').get()).toEqual({ value: 'safe' })
|
expect(stillForeign.prepare('SELECT value FROM canonical').get()).toEqual({ value: 'safe' })
|
||||||
expect(stillForeign.prepare('PRAGMA journal_mode').get()).toEqual({ journal_mode: 'wal' })
|
expect(stillForeign.prepare('PRAGMA journal_mode').get()).toEqual({ journal_mode: 'wal' })
|
||||||
stillForeign.close()
|
stillForeign.close()
|
||||||
await (foreignCtx.sessionQuery as SessionQuerySqlite).close()
|
|
||||||
|
|
||||||
const otherAppPath = await temporaryPath('other-app.db')
|
const otherAppPath = await temporaryPath('other-app.db')
|
||||||
const otherApp = new DatabaseSync(otherAppPath)
|
const otherApp = new DatabaseSync(otherAppPath)
|
||||||
otherApp.exec('PRAGMA application_id = 123')
|
otherApp.exec('PRAGMA application_id = 123')
|
||||||
otherApp.close()
|
otherApp.close()
|
||||||
const otherAppCtx = await liveContext({ path: otherAppPath })
|
const otherAppCtx = new Context()
|
||||||
await expect(otherAppCtx.sessionQuery.searchSessions({ query: 'needle' }))
|
await otherAppCtx.plugin(SessionStore)
|
||||||
|
await expect(otherAppCtx.plugin(SessionQuerySqlite, { path: otherAppPath }))
|
||||||
.rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
|
.rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
|
||||||
await (otherAppCtx.sessionQuery as SessionQuerySqlite).close()
|
expect(otherAppCtx.sessionQuery).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('observes asynchronous open rejection even when no query is made', async () => {
|
it('fails plugin initialization without an unhandled rejection or partial service', async () => {
|
||||||
const path = await temporaryPath('never-queried.db')
|
const path = await temporaryPath('never-queried.db')
|
||||||
const foreign = new DatabaseSync(path)
|
const foreign = new DatabaseSync(path)
|
||||||
foreign.exec('CREATE TABLE canonical(value TEXT)')
|
foreign.exec('CREATE TABLE canonical(value TEXT)')
|
||||||
@@ -1026,10 +1029,13 @@ describe('SQLite schema, cancellation, and real persistence integration', () =>
|
|||||||
const onUnhandled = (reason: unknown) => { unhandled.push(reason) }
|
const onUnhandled = (reason: unknown) => { unhandled.push(reason) }
|
||||||
process.on('unhandledRejection', onUnhandled)
|
process.on('unhandledRejection', onUnhandled)
|
||||||
try {
|
try {
|
||||||
const ctx = await liveContext({ path })
|
const ctx = new Context()
|
||||||
|
await ctx.plugin(SessionStore)
|
||||||
|
await expect(ctx.plugin(SessionQuerySqlite, { path }))
|
||||||
|
.rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
|
||||||
await new Promise<void>((resolve) => { setImmediate(resolve) })
|
await new Promise<void>((resolve) => { setImmediate(resolve) })
|
||||||
expect(unhandled).toEqual([])
|
expect(unhandled).toEqual([])
|
||||||
await (ctx.sessionQuery as SessionQuerySqlite).close()
|
expect(ctx.sessionQuery).toBeUndefined()
|
||||||
} finally {
|
} finally {
|
||||||
process.off('unhandledRejection', onUnhandled)
|
process.off('unhandledRejection', onUnhandled)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user