| 51 | const DEFAULT_LIMIT = 3500; |
| 52 | |
| 53 | export class ChunkedMessageStream { |
| 54 | private buffer = ""; |
| 55 | /** Sorted positions where a chunk ends (= where the next chunk begins). */ |
| 56 | private boundaries: number[] = []; |
| 57 | private streams: MessageStream[] = []; |
| 58 | /** Serialises new-chunk creation so async postPlaceholder calls don't race. */ |
| 59 | private setupPromise: Promise<void> = Promise.resolve(); |
| 60 | private finished = false; |
| 61 | |
| 62 | private readonly limit: number; |
| 63 | private readonly minIntervalMs: number | undefined; |
| 64 | private readonly postPlaceholder: (text: string) => Promise<string>; |
| 65 | private readonly updateAt: (ts: string, text: string) => Promise<void>; |
| 66 | private readonly transform: (text: string) => string; |
| 67 | |
| 68 | constructor(config: ChunkedMessageStreamConfig) { |
| 69 | this.limit = config.limit ?? DEFAULT_LIMIT; |
| 70 | this.minIntervalMs = config.minIntervalMs; |
| 71 | this.postPlaceholder = config.postPlaceholder; |
| 72 | this.updateAt = config.updateAt; |
| 73 | this.transform = config.transform ?? ((t) => t); |
| 74 | } |
| 75 | |
| 76 | append(fullText: string): void { |
| 77 | if (this.finished) return; |
| 78 | if (fullText === this.buffer) return; |
| 79 | this.buffer = fullText; |
| 80 | this.refreezeBoundaries(); |
| 81 | // Make sure we have one Slack message per chunk, then dispatch. |
| 82 | this.setupPromise = this.setupPromise.then(() => |
| 83 | this.ensureStreamsAndDispatch(), |
| 84 | ); |
| 85 | } |
| 86 | |
| 87 | async finish(): Promise<void> { |
| 88 | this.finished = true; |
| 89 | // Drain any pending setup, then a final dispatch, then finish each stream. |
| 90 | this.setupPromise = this.setupPromise.then(() => |
| 91 | this.ensureStreamsAndDispatch(), |
| 92 | ); |
| 93 | await this.setupPromise; |
| 94 | for (const s of this.streams) await s.finish(); |
| 95 | } |
| 96 | |
| 97 | /** Returns the number of Slack messages this stream has posted so far. */ |
| 98 | get chunkCount(): number { |
| 99 | return this.streams.length; |
| 100 | } |
| 101 | |
| 102 | /** |
| 103 | * Walk forward from the last frozen boundary, freezing new ones whenever |
| 104 | * the active chunk's length exceeds the soft limit. Once frozen, a |
| 105 | * boundary doesn't move. |
| 106 | * |
| 107 | * Special case (block-keeps-whole): if the chosen boundary lands INSIDE |
| 108 | * an open fenced code block, we try to move the boundary BACK to the |
| 109 | * position right before the fence opener, so the *whole* block lives in |
| 110 | * the next Slack message rather than being split. The previous chunk |
nothing calls this directly
no test coverage detected
searching dependent graphs…