Просмотр исходного кода

fix(session): scope format registry completeness to per-session decode

buildStepIndex rejected the whole decoder at initialization whenever any
registered step could not reach the current version, so one retired old
upgrader blocked every session, including later versions whose path to
the current version is complete. planSteps already refuses a specific
stored version when a needed step is missing; initialization now checks
only step legality and duplicates.
_Kerman 1 месяц назад
Родитель
Сommit
e8f4315cee

+ 4 - 9
packages/session/session-persistence/src/format-decoder.ts

@@ -145,15 +145,10 @@ function buildStepIndex(steps: readonly SessionFormatStep[]): ReadonlyMap<number
     }
     byFrom.set(step.from, step)
   }
-  for (const first of byFrom.values()) {
-    for (let version = first.from; version < SESSION_FORMAT_VERSION; version++) {
-      if (!byFrom.has(version)) {
-        throw new TypeError(
-          `Session format registry has an incomplete path from v${first.from} to v${SESSION_FORMAT_VERSION}: missing v${version} -> v${version + 1}`,
-        )
-      }
-    }
-  }
+  // A missing step is a per-session concern, decided by planSteps() at decode
+  // time: it refuses sessions at or below the gap, while later versions whose
+  // path to the current version is complete still upgrade. Initialization
+  // therefore checks only step legality and duplicates here.
   return byFrom
 }
 

+ 23 - 3
packages/session/session-persistence/tests/format-decoder.spec.ts

@@ -927,7 +927,7 @@ describe('versioned Session format decoder', { concurrent: false }, () => {
     expect(originalEvents).toEqual(eventSnapshot)
   })
 
-  it('rejects duplicate, invalid, future-targeting, and incomplete static registries at initialization', async () => {
+  it('rejects duplicate, invalid, and future-targeting static registries at initialization', async () => {
     const calls: string[] = []
     await expect(configuredDecoder(1, [migration(0, calls), migration(0, calls)]))
       .rejects.toThrow(/duplicate Session format step/)
@@ -952,8 +952,28 @@ describe('versioned Session format decoder', { concurrent: false }, () => {
 
     await expect(configuredDecoder(1, [migration(1, calls)]))
       .rejects.toThrow(/targets a version newer than this build/)
+  })
 
-    await expect(configuredDecoder(2, [migration(0, calls)]))
-      .rejects.toThrow(/incomplete path/)
+  it('initializes with a gapped registry and refuses only sessions at or below the gap', async () => {
+    const calls: string[] = []
+    const { decodeStoredSession } = await configuredDecoder(
+      3,
+      [migration(0, calls), migration(2, calls)],
+      calls,
+    )
+    const current = storedSource(3, [])
+    const pastGap = storedSource(2, eventLog())
+    const atGap = storedSource(1, eventLog())
+    const belowGap = storedSource(0, eventLog())
+
+    expect(decodeStoredSession(current.source, id).meta.version).toBe(3)
+    const decoded = decodeStoredSession(pastGap.source, id)
+    expect(decoded.sourceVersion).toBe(2)
+    expect(decoded.meta.version).toBe(3)
+    expect(() => decodeStoredSession(atGap.source, id))
+      .toThrow(/missing v1 -> v2/)
+    expect(() => decodeStoredSession(belowGap.source, id))
+      .toThrow(/missing v1 -> v2/)
+    expect(pastGap.reads).toEqual([])
   })
 })