refactor(session-query): narrow phase one to exact reads

This commit is contained in:
Hypatia May
2026-07-11 12:20:35 +08:00
parent 8fd68731ba
commit ad32c57e72
35 changed files with 396 additions and 3036 deletions

View File

@@ -25,7 +25,11 @@ Creates and holds event-sourced `Session` instances. Persistence is intentionall
### Events
The generated [Cordis event catalog](../../../docs/cordis-catalog/events.md) is the signature reference. `session/removed` is an observe-only notification emitted with a cloned header after the entry leaves the store; listener failures cannot fail owner teardown.
| Event | Mode | Purpose |
|---|---|---|
| `session/created` | emit | A session was created |
| `session/event` | emit | An event was appended (sync, fire-and-forget) |
| `session/flush` | parallel | Awaited durability checkpoint (persistence plugins drain buffers here) |
### Class: `Session`

View File

@@ -37,14 +37,6 @@ declare module 'cordis' {
* @mode emit
*/
'session/created'(session: Session): void
/**
* A session left the live store. The header is snapshotted after the store
* entry is removed; listener failures are contained and cannot break the
* owning fiber's teardown.
* @param header - immutable identity and lineage of the removed session.
* @mode parallel
*/
'session/removed'(header: SessionHeader): Promise<void> | void
/**
* An event was appended to a session log (sync, fire-and-forget). This is
* the per-append feed a UI or invariant plugin tails.
@@ -509,15 +501,8 @@ export class SessionStore extends Service {
session.onAppend = (event) => { this.ctx.emit('session/event', session, event) }
this.store.set(session.id, session)
return () => {
if (this.store.get(session.id) !== session) return
session.onAppend = undefined
this.store.delete(session.id)
const header = structuredClone(session.header)
void Promise.resolve()
.then(() => this.ctx.parallel('session/removed', header))
.catch((error: unknown) => {
this.ctx.logger.warn(`session store: session/removed listener failed for "${session.id}": ${String(error)}`)
})
}
}

View File

@@ -350,43 +350,6 @@ describe('SessionStore', () => {
expect(observed).toBe(0)
})
it('announces a cloned header only after the session leaves the store', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const observations: Array<{ id: string; live: boolean }> = []
ctx.on('session/removed', (header) => {
observations.push({ id: header.id, live: ctx.sessions.get(header.id) !== undefined })
header.createdAt = -1
})
const session = ctx.sessions.prepare(SessionId('removed'), { meta: { createdAt: 7 } })
const detach = ctx.sessions.enter(session)
detach()
await Promise.resolve()
await Promise.resolve()
expect(observations).toEqual([{ id: 'removed', live: false }])
expect(session.header.createdAt).toBe(7)
// A repeated disposer cannot remove or announce a later same-id owner.
const replacement = ctx.sessions.create(SessionId('removed'))
detach()
expect(ctx.sessions.get(replacement.id)).toBe(replacement)
expect(observations).toHaveLength(1)
})
it('contains rejected session/removed listeners during teardown', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
ctx.on('session/removed', () => Promise.reject(new Error('observer failed')))
const session = ctx.sessions.prepare(SessionId('contained'))
const detach = ctx.sessions.enter(session)
expect(detach).not.toThrow()
await Promise.resolve()
await Promise.resolve()
expect(ctx.sessions.get(session.id)).toBeUndefined()
})
it('rolls back the session (and onAppend) when a session/created listener throws (P1-1)', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)