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[]>,
|
childrenByParent: ReadonlyMap<SessionId, readonly SessionRecord[]>,
|
||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
): SessionLineageNode[] {
|
): SessionLineageNode[] {
|
||||||
return (childrenByParent.get(sessionId) ?? []).map(child => ({
|
const descendants: SessionLineageNode[] = []
|
||||||
session: cloneRecord(child),
|
const stack = [{ sessionId, descendants }]
|
||||||
descendants: buildDescendants(childrenByParent, child.header.id),
|
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 {
|
function compareSessionsAscending(a: SessionRecord, b: SessionRecord): number {
|
||||||
|
|||||||
@@ -195,6 +195,27 @@ describe('session lineage tracing', () => {
|
|||||||
await expect(ctx.sessionQuery.traceSession(durable.id))
|
await expect(ctx.sessionQuery.traceSession(durable.id))
|
||||||
.rejects.toThrow(expectCode('SESSION_QUERY_PERSISTENCE_FAILED'))
|
.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', () => {
|
describe('session event tracing', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user