fix(session): address restore review feedback
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user