fix(session-query): release stale persistence binding (round 2)
This commit is contained in:
@@ -98,6 +98,7 @@ interface ObservedPersistedSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface PersistenceBinding {
|
interface PersistenceBinding {
|
||||||
|
readonly identity: symbol
|
||||||
readonly service?: SessionPersistence
|
readonly service?: SessionPersistence
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,8 +165,8 @@ export class SessionSearchSqlite extends SessionSearchService {
|
|||||||
private readonly _instance = randomUUID()
|
private readonly _instance = randomUUID()
|
||||||
private readonly _ready: Promise<void>
|
private readonly _ready: Promise<void>
|
||||||
private _db: DatabaseSync | undefined
|
private _db: DatabaseSync | undefined
|
||||||
private _persistenceBinding: PersistenceBinding = {}
|
private _persistenceBinding: PersistenceBinding = { identity: Symbol() }
|
||||||
private _lastPersistenceBinding: PersistenceBinding | undefined
|
private _lastPersistenceIdentity: symbol | undefined
|
||||||
private _persistenceEpoch = 0
|
private _persistenceEpoch = 0
|
||||||
private _globalGeneration = 0
|
private _globalGeneration = 0
|
||||||
private _localGeneration = 0
|
private _localGeneration = 0
|
||||||
@@ -183,12 +184,12 @@ export class SessionSearchSqlite extends SessionSearchService {
|
|||||||
void this._ready.catch(() => undefined)
|
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 = { service }
|
const binding = { identity: Symbol(), service }
|
||||||
this._persistenceBinding = binding
|
this._persistenceBinding = binding
|
||||||
childCtx.effect(() => () => {
|
childCtx.effect(() => () => {
|
||||||
/* v8 ignore next -- a stale optional-service disposer cannot clear a replacement */
|
/* v8 ignore next -- a stale optional-service disposer cannot clear a replacement */
|
||||||
if (this._persistenceBinding !== binding) return
|
if (this._persistenceBinding !== binding) return
|
||||||
this._persistenceBinding = {}
|
this._persistenceBinding = { identity: Symbol() }
|
||||||
}, 'sessionSearchSqlite.persistenceBinding')
|
}, 'sessionSearchSqlite.persistenceBinding')
|
||||||
})
|
})
|
||||||
ctx.effect(() => {
|
ctx.effect(() => {
|
||||||
@@ -335,8 +336,8 @@ export class SessionSearchSqlite extends SessionSearchService {
|
|||||||
: persistedRows.filter(row => !observation.persisted.has(row.id as SessionId))
|
: persistedRows.filter(row => !observation.persisted.has(row.id as SessionId))
|
||||||
const liveChanges = [...observation.live.values()].filter(entry => liveById.get(entry.header.id)?.fingerprint !== entry.fingerprint)
|
const liveChanges = [...observation.live.values()].filter(entry => liveById.get(entry.header.id)?.fingerprint !== entry.fingerprint)
|
||||||
const liveDeletes = liveRows.filter(row => !observation.live.has(row.id as SessionId))
|
const liveDeletes = liveRows.filter(row => !observation.live.has(row.id as SessionId))
|
||||||
const pointerChanged = this._lastPersistenceBinding !== undefined
|
const pointerChanged = this._lastPersistenceIdentity !== undefined
|
||||||
&& this._lastPersistenceBinding !== observation.persistenceBinding
|
&& this._lastPersistenceIdentity !== observation.persistenceBinding.identity
|
||||||
const hasWrites = persistentChanges.length > 0
|
const hasWrites = persistentChanges.length > 0
|
||||||
|| persistentDeletes.length > 0
|
|| persistentDeletes.length > 0
|
||||||
|| liveChanges.length > 0
|
|| liveChanges.length > 0
|
||||||
@@ -390,7 +391,7 @@ export class SessionSearchSqlite extends SessionSearchService {
|
|||||||
if (hasWrites || pointerChanged) this._globalGeneration += 1
|
if (hasWrites || pointerChanged) this._globalGeneration += 1
|
||||||
if (pointerChanged) this._persistenceEpoch += 1
|
if (pointerChanged) this._persistenceEpoch += 1
|
||||||
this._localGeneration = nextLocalGeneration
|
this._localGeneration = nextLocalGeneration
|
||||||
this._lastPersistenceBinding = observation.persistenceBinding
|
this._lastPersistenceIdentity = observation.persistenceBinding.identity
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _observeStable(
|
private async _observeStable(
|
||||||
@@ -404,8 +405,8 @@ export class SessionSearchSqlite extends SessionSearchService {
|
|||||||
let persisted = new Map<SessionId, ObservedPersistedSession>()
|
let persisted = new Map<SessionId, ObservedPersistedSession>()
|
||||||
if (persistence !== undefined) {
|
if (persistence !== undefined) {
|
||||||
try {
|
try {
|
||||||
const canReuseIndexed = this._lastPersistenceBinding === undefined
|
const canReuseIndexed = this._lastPersistenceIdentity === undefined
|
||||||
|| this._lastPersistenceBinding === persistenceBinding
|
|| this._lastPersistenceIdentity === persistenceBinding.identity
|
||||||
const before = await waitWithAbort(persistence.listSnapshots(), signal)
|
const before = await waitWithAbort(persistence.listSnapshots(), signal)
|
||||||
persisted = materializePersistenceSnapshots(before)
|
persisted = materializePersistenceSnapshots(before)
|
||||||
for (const entry of persisted.values()) {
|
for (const entry of persisted.values()) {
|
||||||
|
|||||||
@@ -567,14 +567,17 @@ describe('SQLite reconciliation and source lifecycle', () => {
|
|||||||
const ctx = await liveContext()
|
const ctx = await liveContext()
|
||||||
await ctx.plugin(TestPersistence)
|
await ctx.plugin(TestPersistence)
|
||||||
const internals = ctx.sessionSearch as unknown as {
|
const internals = ctx.sessionSearch as unknown as {
|
||||||
_persistenceBinding: { service?: SessionPersistence }
|
_persistenceBinding: { identity: symbol; service?: SessionPersistence }
|
||||||
}
|
}
|
||||||
const originalList = ctx.sessions.list.bind(ctx.sessions)
|
const originalList = ctx.sessions.list.bind(ctx.sessions)
|
||||||
let bumped = false
|
let bumped = false
|
||||||
const list = vi.spyOn(ctx.sessions, 'list').mockImplementation(() => {
|
const list = vi.spyOn(ctx.sessions, 'list').mockImplementation(() => {
|
||||||
if (!bumped) {
|
if (!bumped) {
|
||||||
bumped = true
|
bumped = true
|
||||||
internals._persistenceBinding = { ...internals._persistenceBinding }
|
internals._persistenceBinding = {
|
||||||
|
...internals._persistenceBinding,
|
||||||
|
identity: Symbol(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return originalList()
|
return originalList()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user