fix(session): address restore review feedback

This commit is contained in:
imccyu
2026-08-06 03:59:01 +08:00
parent 2551b757fb
commit e89b1e612f
11 changed files with 69 additions and 36 deletions

View File

@@ -203,12 +203,18 @@ export function snapshotSessionEvent<T extends SessionEvent>(event: T): T {
return adoptSessionEvent(structuredClone(event))
}
/** Deep-freeze one acyclic object tree materialized by JSON parsing. input is stackoverflow-safe */
function freezeRestoredObject<T>(value: T): T {
Object.freeze(value)
for (const key in value) {
const child = (value as Record<string, unknown>)[key]
if (child !== null && typeof child === 'object') freezeRestoredObject(child)
/** Deep-freeze one acyclic JSON tree without consuming the JavaScript call stack. */
function freezeRestoredObject<T extends object>(value: T): T {
const pending: object[] = [value]
while (pending.length > 0) {
// The non-empty check proves an object remains to visit.
// oxlint-disable-next-line typescript/no-non-null-assertion
const current = pending.pop()!
Object.freeze(current)
for (const key in current) {
const child = (current as Record<string, unknown>)[key]
if (child !== null && typeof child === 'object') pending.push(child)
}
}
return value
}

View File

@@ -941,6 +941,36 @@ describe('Session', () => {
expect(() => { appendedEvent.data.todos[0]!.content = 'mutated' }).toThrow(TypeError)
})
it('iteratively freezes deeply nested restored event data', () => {
const depth = 20_000
const data: Record<string, unknown> = {}
let tail = data
for (let index = 0; index < depth; index += 1) {
const child: Record<string, unknown> = {}
tail['child'] = child
tail = child
}
const event = {
type: 'test/deep-restore', seq: 0, time: 1, data,
} as unknown as SessionEvent
expect(() => Session.fromRestore(SessionId('deep-restore'), [event], {
version: SESSION_FORMAT_VERSION,
id: SessionId('deep-restore'),
createdAt: 1,
})).not.toThrow()
let current: unknown = event
let frozenNodes = 0
for (let index = 0; index <= depth + 1; index += 1) {
if (!Object.isFrozen(current)) break
frozenNodes += 1
current = (current as Record<string, unknown>)['data']
?? (current as Record<string, unknown>)['child']
}
expect(frozenNodes).toBe(depth + 2)
})
it('returns cached frozen event-array snapshots that do not grow after append', () => {
const session = Session.create(SessionId('events-snapshot'))
session.append('turn/start', { turn: 1 })