| 110 | * backend mirrors. |
| 111 | */ |
| 112 | export class InMemoryRunEventLog implements RunEventLog { |
| 113 | private readonly runs = new Map<string, RunState>() |
| 114 | |
| 115 | private now(): number { |
| 116 | return Date.now() |
| 117 | } |
| 118 | |
| 119 | private require(runId: string): RunState { |
| 120 | const state = this.runs.get(runId) |
| 121 | if (!state) throw new Error(`run-log: unknown runId "${runId}"`) |
| 122 | return state |
| 123 | } |
| 124 | |
| 125 | private wake(state: RunState): void { |
| 126 | const waiters = [...state.waiters] |
| 127 | state.waiters.clear() |
| 128 | for (const resolve of waiters) resolve() |
| 129 | } |
| 130 | |
| 131 | // Mutators return a Promise without `async` so contract violations REJECT |
| 132 | // (rather than throwing synchronously from a Promise-typed method — a |
| 133 | // `.catch()` footgun) without an `await`-less async body. |
| 134 | open(input: { runId: string; threadId?: string }): Promise<RunRecord> { |
| 135 | const existing = this.runs.get(input.runId) |
| 136 | if (existing) return Promise.resolve({ ...existing.record }) |
| 137 | const now = this.now() |
| 138 | const record: RunRecord = { |
| 139 | runId: input.runId, |
| 140 | ...(input.threadId !== undefined ? { threadId: input.threadId } : {}), |
| 141 | status: 'running', |
| 142 | lastSeq: -1, |
| 143 | createdAt: now, |
| 144 | updatedAt: now, |
| 145 | } |
| 146 | this.runs.set(input.runId, { record, chunks: [], waiters: new Set() }) |
| 147 | return Promise.resolve({ ...record }) |
| 148 | } |
| 149 | |
| 150 | append(runId: string, chunk: StreamChunk): Promise<number> { |
| 151 | const state = this.runs.get(runId) |
| 152 | if (!state) { |
| 153 | return Promise.reject(new Error(`run-log: unknown runId "${runId}"`)) |
| 154 | } |
| 155 | if (isTerminalRunStatus(state.record.status)) { |
| 156 | return Promise.reject( |
| 157 | new Error( |
| 158 | `run-log: cannot append to terminal run "${runId}" (status=${state.record.status})`, |
| 159 | ), |
| 160 | ) |
| 161 | } |
| 162 | // Derive seq from the record's cursor (not `chunks.length`) so the gap-free |
| 163 | // invariant holds the same way the durable backend computes it, even if the |
| 164 | // backlog is ever trimmed/compacted. |
| 165 | const seq = state.record.lastSeq + 1 |
| 166 | state.chunks.push(chunk) |
| 167 | state.record.lastSeq = seq |
| 168 | state.record.updatedAt = this.now() |
| 169 | this.wake(state) |
nothing calls this directly
no outgoing calls
no test coverage detected