A tool for people curious about the React Server Components protocol rscexplorer.dev/
rsc react
at main 164 lines 4.2 kB view raw
1import type { SteppableStream, Thenable } from "./steppable-stream.ts"; 2 3type InternalEntry = 4 | { type: "render"; stream: SteppableStream } 5 | { type: "action"; name: string; args: string; stream: SteppableStream }; 6 7export type RowView = { 8 display: string; 9 hexStart: number; 10}; 11 12export type EntryView = { 13 type: "render" | "action"; 14 name?: string; 15 args?: string; 16 rows: readonly RowView[]; 17 flightPromise: Thenable<unknown> | undefined; 18 error: Error | null; 19 chunkStart: number; 20 chunkCount: number; 21 canDelete: boolean; 22 isActive: boolean; 23 isDone: boolean; 24}; 25 26export interface TimelineSnapshot { 27 entries: EntryView[]; 28 cursor: number; 29 totalChunks: number; 30 isAtStart: boolean; 31 isAtEnd: boolean; 32 isStreaming: boolean; 33} 34 35type Listener = () => void; 36 37export class Timeline { 38 private entries: InternalEntry[] = []; 39 private cursor = 0; 40 private listeners = new Set<Listener>(); 41 private cachedSnapshot: TimelineSnapshot | null = null; 42 43 private notify(): void { 44 this.cachedSnapshot = null; 45 for (const fn of this.listeners) { 46 fn(); 47 } 48 } 49 50 subscribe = (listener: Listener): (() => void) => { 51 this.listeners.add(listener); 52 return () => this.listeners.delete(listener); 53 }; 54 55 getSnapshot = (): TimelineSnapshot => { 56 if (this.cachedSnapshot) { 57 return this.cachedSnapshot; 58 } 59 60 let chunkStart = 0; 61 const entries: EntryView[] = this.entries.map((entry) => { 62 const { stream } = entry; 63 const chunkCount = stream.rows.length; 64 const chunkEnd = chunkStart + chunkCount; 65 const base = { 66 rows: stream.rows.map((r) => ({ display: r.display, hexStart: r.hexStart })), 67 flightPromise: stream.flightPromise, 68 error: stream.error, 69 chunkStart, 70 chunkCount, 71 canDelete: this.cursor <= chunkStart, 72 isActive: this.cursor >= chunkStart && this.cursor < chunkEnd, 73 isDone: this.cursor >= chunkEnd, 74 }; 75 chunkStart = chunkEnd; 76 if (entry.type === "action") { 77 return { type: "action" as const, name: entry.name, args: entry.args, ...base }; 78 } 79 return { type: "render" as const, ...base }; 80 }); 81 82 this.cachedSnapshot = { 83 entries, 84 cursor: this.cursor, 85 totalChunks: chunkStart, 86 isAtStart: this.cursor === 0, 87 isAtEnd: this.cursor >= chunkStart, 88 isStreaming: this.entries.some((e) => !e.stream.done), 89 }; 90 return this.cachedSnapshot; 91 }; 92 93 setRender = (stream: SteppableStream): void => { 94 this.entries = [{ type: "render", stream }]; 95 this.cursor = 0; 96 this.watchStream(stream); 97 this.notify(); 98 }; 99 100 addAction = (name: string, args: string, stream: SteppableStream): void => { 101 this.entries = [...this.entries, { type: "action", name, args, stream }]; 102 this.watchStream(stream); 103 this.notify(); 104 }; 105 106 private async watchStream(stream: SteppableStream): Promise<void> { 107 try { 108 for await (const _ of stream) { 109 this.notify(); 110 } 111 this.notify(); 112 } catch { 113 this.notify(); 114 } 115 } 116 117 deleteEntry = (entryIndex: number): void => { 118 let chunkStart = 0; 119 for (let i = 0; i < entryIndex; i++) { 120 chunkStart += this.entries[i]!.stream.rows.length; 121 } 122 if (this.cursor > chunkStart) { 123 return; 124 } 125 this.entries = this.entries.filter((_, i) => i !== entryIndex); 126 this.notify(); 127 }; 128 129 stepForward = (): void => { 130 let remaining = this.cursor; 131 for (const entry of this.entries) { 132 const count = entry.stream.rows.length; 133 if (remaining < count) { 134 entry.stream.release(remaining + 1); 135 this.cursor++; 136 this.notify(); 137 return; 138 } 139 remaining -= count; 140 } 141 }; 142 143 skipToEntryEnd = (): void => { 144 let remaining = this.cursor; 145 for (const entry of this.entries) { 146 const count = entry.stream.rows.length; 147 if (remaining < count) { 148 for (let local = remaining; local < count; local++) { 149 entry.stream.release(local + 1); 150 } 151 this.cursor += count - remaining; 152 this.notify(); 153 return; 154 } 155 remaining -= count; 156 } 157 }; 158 159 clear = (): void => { 160 this.entries = []; 161 this.cursor = 0; 162 this.notify(); 163 }; 164}