| 1 | import { resolvedHistoryField } from "./canonicalTranscriptBackend"; |
| 2 | import { applyResolvedField, type TranscriptRecord } from "./transcriptRecordProjection"; |
| 3 | import { recordBytes } from "./transcriptRecordBytes"; |
| 4 | import type { SessionTranscript } from "./transcriptStoreTypes"; |
| 5 | import type { HistoryContentChunk, HistoryContentRef } from "./types"; |
| 6 | |
| 7 | interface ContentReadOwner { |
| 8 | locate(entryId: string): { session: SessionTranscript; entryId: string } | undefined; |
| 9 | resident(session: SessionTranscript): boolean; |
| 10 | read(ref: HistoryContentRef, index: number): Promise<HistoryContentChunk>; |
| 11 | publish(session: SessionTranscript, record: TranscriptRecord): void; |
| 12 | } |
| 13 | |
| 14 | /** Resolve against one resident owner, with one handoff to its replacement cut. */ |
| 15 | export async function readTranscriptContent( |
| 16 | owner: ContentReadOwner, alias: string, field: string, |
| 17 | previous?: SessionTranscript, handoff = true, |
| 18 | ): Promise<string | undefined> { |
| 19 | const location = owner.locate(alias); |
| 20 | if (!location || (previous && previous !== location.session)) return undefined; |
| 21 | const { session, entryId } = location; |
| 22 | const generation = session.generation; |
| 23 | const settlement = session.generationSettlement; |
| 24 | if (settlement?.generation === generation) await settlement.promise; |
| 25 | if (!owner.resident(session) || generation !== session.generation) return undefined; |
| 26 | const rec = session.byId.get(entryId); |
| 27 | if (!rec) return undefined; |
| 28 | if (rec.resolved?.[field] !== undefined) return rec.resolved[field]; |
| 29 | const ref = rec.refs.find(candidate => candidate.field === field || candidate.field === "canonicalMessage"); |
| 30 | if (!ref) return undefined; |
| 31 | const pendingKey = JSON.stringify([entryId, field]); |
| 32 | const pending = session.pendingContent.get(pendingKey); |
| 33 | if (pending?.generation === generation) return pending.promise; |
| 34 | const request = (async (): Promise<string | undefined> => { |
| 35 | let data = ""; |
| 36 | for (let index = 0; index < Math.max(1, ref.chunks); index++) { |
| 37 | const chunk = await owner.read(ref, index); |
| 38 | // Closing/evicting is terminal, including a reopened tab with the same ID. |
| 39 | if (!owner.resident(session)) return undefined; |
| 40 | if (session.generation !== generation) { |
| 41 | return handoff ? readTranscriptContent(owner, alias, field, session, false) : undefined; |
| 42 | } |
| 43 | if (session.byId.get(entryId) !== rec) return undefined; |
| 44 | if (chunk.stale) { |
| 45 | rec.staleRefs = { ...rec.staleRefs, [field]: true }; |
| 46 | return undefined; |
| 47 | } |
| 48 | data += chunk.data ?? ""; |
| 49 | if (chunk.done) break; |
| 50 | } |
| 51 | if (!applyResolvedField(rec, ref, data)) return undefined; |
| 52 | const previousBytes = rec.bytes; |
| 53 | rec.bytes = recordBytes(rec.message); |
| 54 | session.bodyBytes += rec.bytes - previousBytes; |
| 55 | const value = ref.field === "canonicalMessage" ? resolvedHistoryField(rec.message, field) : data; |
| 56 | if (value === undefined) return undefined; |
| 57 | rec.resolved = { ...rec.resolved, [field]: value }; |
| 58 | owner.publish(session, rec); |
| 59 | return value; |
| 60 | })(); |
| 61 | const entry = { generation, promise: request }; |
| 62 | const release = () => { |
| 63 | if (session.pendingContent.get(pendingKey) === entry) session.pendingContent.delete(pendingKey); |
| 64 | }; |
| 65 | session.pendingContent.set(pendingKey, entry); |
| 66 | void request.then(release, release); |
| 67 | return request; |
| 68 | } |
| 69 |