fix(session-query): avoid deep lineage recursion (round 2)
This commit is contained in:
@@ -248,10 +248,26 @@ function buildDescendants(
|
||||
childrenByParent: ReadonlyMap<SessionId, readonly SessionRecord[]>,
|
||||
sessionId: SessionId,
|
||||
): SessionLineageNode[] {
|
||||
return (childrenByParent.get(sessionId) ?? []).map(child => ({
|
||||
session: cloneRecord(child),
|
||||
descendants: buildDescendants(childrenByParent, child.header.id),
|
||||
}))
|
||||
const descendants: SessionLineageNode[] = []
|
||||
const stack = [{ sessionId, descendants }]
|
||||
while (stack.length > 0) {
|
||||
// The length guard proves a frame exists.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const frame = stack.pop()!
|
||||
const nodes: SessionLineageNode[] = []
|
||||
for (const child of childrenByParent.get(frame.sessionId) ?? []) {
|
||||
const node = { session: cloneRecord(child), descendants: [] }
|
||||
nodes.push(node)
|
||||
frame.descendants.push(node)
|
||||
}
|
||||
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
||||
// The loop bounds prove this indexed node exists.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const node = nodes[index]!
|
||||
stack.push({ sessionId: node.session.header.id, descendants: node.descendants })
|
||||
}
|
||||
}
|
||||
return descendants
|
||||
}
|
||||
|
||||
function compareSessionsAscending(a: SessionRecord, b: SessionRecord): number {
|
||||
|
||||
@@ -195,6 +195,27 @@ describe('session lineage tracing', () => {
|
||||
await expect(ctx.sessionQuery.traceSession(durable.id))
|
||||
.rejects.toThrow(expectCode('SESSION_QUERY_PERSISTENCE_FAILED'))
|
||||
})
|
||||
|
||||
it('constructs deeply nested descendants without consuming the JavaScript call stack', async () => {
|
||||
const ctx = await queryContext()
|
||||
const root = ctx.sessions.create(SessionId('deep-0'), { meta: { createdAt: 0 } })
|
||||
let parent = root
|
||||
for (let depth = 1; depth < 3_000; depth += 1) {
|
||||
parent = ctx.sessions.create(SessionId(`deep-${depth}`), {
|
||||
meta: { createdAt: depth, parentSession: parent.id },
|
||||
})
|
||||
}
|
||||
|
||||
const trace = await ctx.sessionQuery.traceSession(root.id)
|
||||
expect(trace.complete).toBe(true)
|
||||
let node = trace.descendants[0]
|
||||
for (let depth = 1; depth < 3_000; depth += 1) {
|
||||
if (node === undefined) throw new Error(`lineage ended before depth ${depth}`)
|
||||
if (depth === 2_999) expect(node.session.header.id).toBe(SessionId('deep-2999'))
|
||||
node = node.descendants[0]
|
||||
}
|
||||
expect(node).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('session event tracing', () => {
|
||||
|
||||
Reference in New Issue
Block a user