diff --git a/packages/session-projection/session-projection-cache/src/index.ts b/packages/session-projection/session-projection-cache/src/index.ts index abb39ad1de..92515015f8 100644 --- a/packages/session-projection/session-projection-cache/src/index.ts +++ b/packages/session-projection/session-projection-cache/src/index.ts @@ -141,8 +141,14 @@ export class SessionProjectionCache extends Service { async coldSnapshot(id: SessionId, signal?: AbortSignal): Promise { const cached = this.checkpointOf(id) const floor = this.ctx.sessionProjections.restoreFloor(cached) - if (floor === undefined) return { asOfSeq: -1, values: {} } const persistence = this.ctx.sessionPersistence + if (floor === undefined) { + // No unit registered: nothing to fold, but the not-found contract must + // hold in this topology too — the probe read rejects for an absent log + // and dates the empty cut for a present one. + const probe = await persistence.readFrom(id, 0, signal) + return { asOfSeq: probe.events.at(-1)?.seq ?? -1, values: {} } + } let restored: { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } const tail = await persistence.readFrom(id, floor, signal) try { diff --git a/packages/session-projection/session-projection-cache/tests/cache.spec.ts b/packages/session-projection/session-projection-cache/tests/cache.spec.ts index defeddfd32..33d82cea24 100644 --- a/packages/session-projection/session-projection-cache/tests/cache.spec.ts +++ b/packages/session-projection/session-projection-cache/tests/cache.spec.ts @@ -252,4 +252,26 @@ describe('SessionProjectionCache cold read', () => { const { cache } = await harness() await expect(cache.coldSnapshot(SessionId('absent'))).rejects.toThrow('not found') }) + + it('holds the not-found contract with zero registered units, and dates the empty cut for a present log', async () => { + // Same composition minus any registered unit: restoreFloor is undefined, + // yet coldSnapshot must still reject for an absent log (probe read) and + // serve an empty cut at the stored end for a present one. + const pool = new MemoryMediaPool() + const logs = new Map([['bare', storedLog([['a']])]]) // seqs 0..2 + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(Storage) + ctx.storage.backend.register('memory', new MemoryStorageBackend(pool)) + const facility = new DomainFacility(ctx, { backend: 'memory', routes: {} }) + ctx.storage.mount('domain', facility) + ctx.provide('storageDomain', facility) + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + ctx.provide('sessionPersistence', fakePersistence(logs) as never) + await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 }) + await expect(ctx.sessionProjectionCache.coldSnapshot(SessionId('absent'))).rejects.toThrow('not found') + await expect(ctx.sessionProjectionCache.coldSnapshot(SessionId('bare'))) + .resolves.toEqual({ asOfSeq: 2, values: {} }) + }) })