source dump of claude code
at main 233 lines 6.0 kB view raw
1import { 2 ContinuousEventPriority, 3 DefaultEventPriority, 4 DiscreteEventPriority, 5 NoEventPriority, 6} from 'react-reconciler/constants.js' 7import { logError } from '../../utils/log.js' 8import { HANDLER_FOR_EVENT } from './event-handlers.js' 9import type { EventTarget, TerminalEvent } from './terminal-event.js' 10 11// -- 12 13type DispatchListener = { 14 node: EventTarget 15 handler: (event: TerminalEvent) => void 16 phase: 'capturing' | 'at_target' | 'bubbling' 17} 18 19function getHandler( 20 node: EventTarget, 21 eventType: string, 22 capture: boolean, 23): ((event: TerminalEvent) => void) | undefined { 24 const handlers = node._eventHandlers 25 if (!handlers) return undefined 26 27 const mapping = HANDLER_FOR_EVENT[eventType] 28 if (!mapping) return undefined 29 30 const propName = capture ? mapping.capture : mapping.bubble 31 if (!propName) return undefined 32 33 return handlers[propName] as ((event: TerminalEvent) => void) | undefined 34} 35 36/** 37 * Collect all listeners for an event in dispatch order. 38 * 39 * Uses react-dom's two-phase accumulation pattern: 40 * - Walk from target to root 41 * - Capture handlers are prepended (unshift) → root-first 42 * - Bubble handlers are appended (push) → target-first 43 * 44 * Result: [root-cap, ..., parent-cap, target-cap, target-bub, parent-bub, ..., root-bub] 45 */ 46function collectListeners( 47 target: EventTarget, 48 event: TerminalEvent, 49): DispatchListener[] { 50 const listeners: DispatchListener[] = [] 51 52 let node: EventTarget | undefined = target 53 while (node) { 54 const isTarget = node === target 55 56 const captureHandler = getHandler(node, event.type, true) 57 const bubbleHandler = getHandler(node, event.type, false) 58 59 if (captureHandler) { 60 listeners.unshift({ 61 node, 62 handler: captureHandler, 63 phase: isTarget ? 'at_target' : 'capturing', 64 }) 65 } 66 67 if (bubbleHandler && (event.bubbles || isTarget)) { 68 listeners.push({ 69 node, 70 handler: bubbleHandler, 71 phase: isTarget ? 'at_target' : 'bubbling', 72 }) 73 } 74 75 node = node.parentNode 76 } 77 78 return listeners 79} 80 81/** 82 * Execute collected listeners with propagation control. 83 * 84 * Before each handler, calls event._prepareForTarget(node) so event 85 * subclasses can do per-node setup. 86 */ 87function processDispatchQueue( 88 listeners: DispatchListener[], 89 event: TerminalEvent, 90): void { 91 let previousNode: EventTarget | undefined 92 93 for (const { node, handler, phase } of listeners) { 94 if (event._isImmediatePropagationStopped()) { 95 break 96 } 97 98 if (event._isPropagationStopped() && node !== previousNode) { 99 break 100 } 101 102 event._setEventPhase(phase) 103 event._setCurrentTarget(node) 104 event._prepareForTarget(node) 105 106 try { 107 handler(event) 108 } catch (error) { 109 logError(error) 110 } 111 112 previousNode = node 113 } 114} 115 116// -- 117 118/** 119 * Map terminal event types to React scheduling priorities. 120 * Mirrors react-dom's getEventPriority() switch. 121 */ 122function getEventPriority(eventType: string): number { 123 switch (eventType) { 124 case 'keydown': 125 case 'keyup': 126 case 'click': 127 case 'focus': 128 case 'blur': 129 case 'paste': 130 return DiscreteEventPriority as number 131 case 'resize': 132 case 'scroll': 133 case 'mousemove': 134 return ContinuousEventPriority as number 135 default: 136 return DefaultEventPriority as number 137 } 138} 139 140// -- 141 142type DiscreteUpdates = <A, B>( 143 fn: (a: A, b: B) => boolean, 144 a: A, 145 b: B, 146 c: undefined, 147 d: undefined, 148) => boolean 149 150/** 151 * Owns event dispatch state and the capture/bubble dispatch loop. 152 * 153 * The reconciler host config reads currentEvent and currentUpdatePriority 154 * to implement resolveUpdatePriority, resolveEventType, and 155 * resolveEventTimeStamp — mirroring how react-dom's host config reads 156 * ReactDOMSharedInternals and window.event. 157 * 158 * discreteUpdates is injected after construction (by InkReconciler) 159 * to break the import cycle. 160 */ 161export class Dispatcher { 162 currentEvent: TerminalEvent | null = null 163 currentUpdatePriority: number = DefaultEventPriority as number 164 discreteUpdates: DiscreteUpdates | null = null 165 166 /** 167 * Infer event priority from the currently-dispatching event. 168 * Called by the reconciler host config's resolveUpdatePriority 169 * when no explicit priority has been set. 170 */ 171 resolveEventPriority(): number { 172 if (this.currentUpdatePriority !== (NoEventPriority as number)) { 173 return this.currentUpdatePriority 174 } 175 if (this.currentEvent) { 176 return getEventPriority(this.currentEvent.type) 177 } 178 return DefaultEventPriority as number 179 } 180 181 /** 182 * Dispatch an event through capture and bubble phases. 183 * Returns true if preventDefault() was NOT called. 184 */ 185 dispatch(target: EventTarget, event: TerminalEvent): boolean { 186 const previousEvent = this.currentEvent 187 this.currentEvent = event 188 try { 189 event._setTarget(target) 190 191 const listeners = collectListeners(target, event) 192 processDispatchQueue(listeners, event) 193 194 event._setEventPhase('none') 195 event._setCurrentTarget(null) 196 197 return !event.defaultPrevented 198 } finally { 199 this.currentEvent = previousEvent 200 } 201 } 202 203 /** 204 * Dispatch with discrete (sync) priority. 205 * For user-initiated events: keyboard, click, focus, paste. 206 */ 207 dispatchDiscrete(target: EventTarget, event: TerminalEvent): boolean { 208 if (!this.discreteUpdates) { 209 return this.dispatch(target, event) 210 } 211 return this.discreteUpdates( 212 (t, e) => this.dispatch(t, e), 213 target, 214 event, 215 undefined, 216 undefined, 217 ) 218 } 219 220 /** 221 * Dispatch with continuous priority. 222 * For high-frequency events: resize, scroll, mouse move. 223 */ 224 dispatchContinuous(target: EventTarget, event: TerminalEvent): boolean { 225 const previousPriority = this.currentUpdatePriority 226 try { 227 this.currentUpdatePriority = ContinuousEventPriority as number 228 return this.dispatch(target, event) 229 } finally { 230 this.currentUpdatePriority = previousPriority 231 } 232 } 233}