|
| 1 | +# Debugging Request/Response Timeout Issues |
| 2 | + |
| 3 | +This guide explains how to debug timeout errors in the REQUEST/RESPONSE pattern used between React components and NotePlan plugins. |
| 4 | + |
| 5 | +## Error Symptoms |
| 6 | + |
| 7 | +``` |
| 8 | +[WebView Log] DEBUG | Component, requestFromPlugin TIMEOUT: command="getXXX", correlationId="req-..." |
| 9 | +[WebView Error] ERROR | Component, requestFromPlugin REJECTED: command="getXXX", error="Request timeout: getXXX" |
| 10 | +[WebView Error] ERROR | Component, Error loading data: Request timeout: getXXX |
| 11 | +``` |
| 12 | + |
| 13 | +## Common Causes |
| 14 | + |
| 15 | +### 1. Outdated Local routerUtils.js (MOST COMMON) |
| 16 | + |
| 17 | +**Problem**: Plugin has a local copy of `routerUtils.js` that's out of sync with the shared version in `@helpers/react/routerUtils`. |
| 18 | + |
| 19 | +**Symptoms**: |
| 20 | +- Works for some users but not others |
| 21 | +- No error logs on plugin side |
| 22 | +- Silent failures in response sending |
| 23 | + |
| 24 | +**Fix**: |
| 25 | +```bash |
| 26 | +# Delete local copy |
| 27 | +rm src/routerUtils.js |
| 28 | + |
| 29 | +# Update imports to use shared version |
| 30 | +# In your router file (e.g., favoritesRouter.js): |
| 31 | +import { newCommsRouter, type RequestResponse } from '@helpers/react/routerUtils' |
| 32 | +import pluginJson from '../plugin.json' |
| 33 | + |
| 34 | +export const onYourRouter = newCommsRouter({ |
| 35 | + routerName: 'YourRouter', |
| 36 | + defaultWindowId: YOUR_WINDOW_ID, |
| 37 | + routeRequest: routeYourRequest, |
| 38 | + handleNonRequestAction: handleYourNonRequestAction, |
| 39 | + pluginJson: pluginJson, // REQUIRED! |
| 40 | + useSharedHandlersFallback: false, |
| 41 | +}) |
| 42 | +``` |
| 43 | +
|
| 44 | +**Prevention**: |
| 45 | +- ✅ Always use `@helpers/react/routerUtils` (shared version) |
| 46 | +- ❌ Never copy routerUtils.js to your plugin |
| 47 | +- ✅ Always pass `pluginJson` parameter to `newCommsRouter` |
| 48 | +
|
| 49 | +### 2. Handler Function Crashes |
| 50 | +
|
| 51 | +**Problem**: The handler throws an exception that's not caught properly. |
| 52 | +
|
| 53 | +**Symptoms**: |
| 54 | +- Error logs on plugin side |
| 55 | +- No RESPONSE sent back to React |
| 56 | +- Timeout after 10 seconds |
| 57 | +
|
| 58 | +**Debug**: |
| 59 | +```javascript |
| 60 | +// In your handler (requestHandlers.js): |
| 61 | +export async function handleGetData(requestData: Object): Promise<RequestResponse> { |
| 62 | + const startTime = Date.now() |
| 63 | + try { |
| 64 | + logDebug(pluginJson, `handleGetData: ENTRY`) |
| 65 | + |
| 66 | + // Your code here |
| 67 | + const data = await someOperation() |
| 68 | + |
| 69 | + const elapsed = Date.now() - startTime |
| 70 | + logDebug(pluginJson, `handleGetData: SUCCESS in ${elapsed}ms`) |
| 71 | + return { |
| 72 | + success: true, |
| 73 | + data: data, |
| 74 | + } |
| 75 | + } catch (error) { |
| 76 | + const elapsed = Date.now() - startTime |
| 77 | + logError(pluginJson, `handleGetData: ERROR after ${elapsed}ms: ${error.message}`) |
| 78 | + return { |
| 79 | + success: false, |
| 80 | + message: error.message || 'Failed to get data', |
| 81 | + } |
| 82 | + } |
| 83 | +} |
| 84 | +``` |
| 85 | +
|
| 86 | +**Fix**: |
| 87 | +- Always use try/catch in handlers |
| 88 | +- Always return a RequestResponse object |
| 89 | +- Log timing to identify slow operations |
| 90 | +
|
| 91 | +### 3. Wrong Window ID |
| 92 | +
|
| 93 | +**Problem**: Response is sent to wrong window ID, so React never receives it. |
| 94 | +
|
| 95 | +**Symptoms**: |
| 96 | +- Plugin logs show success |
| 97 | +- React times out |
| 98 | +- Multiple windows open |
| 99 | +
|
| 100 | +**Debug**: |
| 101 | +```javascript |
| 102 | +// In your router: |
| 103 | +export const onYourRouter = newCommsRouter({ |
| 104 | + routerName: 'YourRouter', |
| 105 | + defaultWindowId: 'your-window-id', // Must match React window ID |
| 106 | + routeRequest: routeYourRequest, |
| 107 | + pluginJson: pluginJson, |
| 108 | +}) |
| 109 | +
|
| 110 | +// In React (check what windowId is being sent): |
| 111 | +const requestFromPlugin = useCallback( |
| 112 | + (command: string, dataToSend: any = {}, timeout: number = 10000): Promise<any> => { |
| 113 | + const correlationId = `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` |
| 114 | + logDebug('Component', `requestFromPlugin: command="${command}", windowId="${windowIdRef.current}"`) |
| 115 | + |
| 116 | + // ... |
| 117 | + }, |
| 118 | + [dispatch], |
| 119 | +) |
| 120 | +``` |
| 121 | +
|
| 122 | +**Fix**: |
| 123 | +- Ensure `defaultWindowId` in router matches window ID used when opening window |
| 124 | +- Check `windowIdRef.current` in React component |
| 125 | +- Verify `data.__windowId` is set correctly in requests |
| 126 | +
|
| 127 | +### 4. Router Not Registered in plugin.json |
| 128 | +
|
| 129 | +**Problem**: Router function not exported or not registered as a command. |
| 130 | +
|
| 131 | +**Symptoms**: |
| 132 | +- No logs on plugin side |
| 133 | +- Immediate or delayed timeout |
| 134 | +- "Command not found" errors |
| 135 | +
|
| 136 | +**Debug**: |
| 137 | +```json |
| 138 | +// Check plugin.json has the router command: |
| 139 | +{ |
| 140 | + "plugin.commands": [ |
| 141 | + { |
| 142 | + "name": "onYourRouter", |
| 143 | + "description": "React Window calling back to plugin", |
| 144 | + "jsFunction": "onYourRouter", |
| 145 | + "hidden": true |
| 146 | + } |
| 147 | + ] |
| 148 | +} |
| 149 | +``` |
| 150 | +
|
| 151 | +```javascript |
| 152 | +// Check src/index.js exports the router: |
| 153 | +export { onYourRouter } from './yourRouter.js' |
| 154 | +``` |
| 155 | +
|
| 156 | +**Fix**: |
| 157 | +- Ensure router is exported from index.js |
| 158 | +- Ensure router is registered in plugin.json |
| 159 | +- Verify `jsFunction` name matches export name |
| 160 | +
|
| 161 | +### 5. Handler Takes Too Long (> 10 seconds) |
| 162 | +
|
| 163 | +**Problem**: Operation genuinely takes longer than timeout. |
| 164 | +
|
| 165 | +**Symptoms**: |
| 166 | +- Happens consistently for certain operations |
| 167 | +- Logs show handler is still running when timeout occurs |
| 168 | +- Large datasets or slow APIs |
| 169 | +
|
| 170 | +**Fix**: |
| 171 | +```javascript |
| 172 | +// Option 1: Increase timeout for specific requests |
| 173 | +const data = await requestFromPlugin('slowOperation', {}, 30000) // 30 second timeout |
| 174 | +
|
| 175 | +// Option 2: Optimize the handler |
| 176 | +export async function handleSlowOperation(requestData: Object): Promise<RequestResponse> { |
| 177 | + try { |
| 178 | + // Add early filtering/pagination |
| 179 | + const limit = requestData.limit || 100 |
| 180 | + const notes = DataStore.projectNotes.slice(0, limit) |
| 181 | + |
| 182 | + // Process in chunks if needed |
| 183 | + // ... |
| 184 | + |
| 185 | + return { success: true, data: notes } |
| 186 | + } catch (error) { |
| 187 | + return { success: false, message: error.message } |
| 188 | + } |
| 189 | +} |
| 190 | +``` |
| 191 | +
|
| 192 | +### 6. React Component Unmounted Before Response |
| 193 | +
|
| 194 | +**Problem**: React component unmounts while waiting for response. |
| 195 | +
|
| 196 | +**Symptoms**: |
| 197 | +- Happens when rapidly switching views |
| 198 | +- No error logs |
| 199 | +- Memory leaks (pending requests not cleaned up) |
| 200 | +
|
| 201 | +**Fix**: |
| 202 | +```javascript |
| 203 | +// In React component, ensure cleanup in useEffect: |
| 204 | +useEffect(() => { |
| 205 | + const handleResponse = (event: MessageEvent) => { |
| 206 | + // Handle response |
| 207 | + } |
| 208 | + |
| 209 | + window.addEventListener('message', handleResponse) |
| 210 | + return () => { |
| 211 | + window.removeEventListener('message', handleResponse) |
| 212 | + // Clean up pending requests |
| 213 | + pendingRequestsRef.current.forEach((pending) => { |
| 214 | + clearTimeout(pending.timeoutId) |
| 215 | + }) |
| 216 | + pendingRequestsRef.current.clear() |
| 217 | + } |
| 218 | +}, []) |
| 219 | +``` |
| 220 | +
|
| 221 | +## Debugging Checklist |
| 222 | +
|
| 223 | +When a user reports timeout errors: |
| 224 | +
|
| 225 | +1. ✅ Check if plugin has local `routerUtils.js` - delete it if found |
| 226 | +2. ✅ Verify router imports from `@helpers/react/routerUtils` |
| 227 | +3. ✅ Verify `pluginJson` parameter is passed to `newCommsRouter` |
| 228 | +4. ✅ Check router is exported from `src/index.js` |
| 229 | +5. ✅ Check router is registered in `plugin.json` |
| 230 | +6. ✅ Verify window IDs match (router default vs React windowId) |
| 231 | +7. ✅ Check handler has try/catch and returns RequestResponse |
| 232 | +8. ✅ Check handler case is listed in router's switch statement |
| 233 | +9. ✅ Add timing logs to handler to identify slow operations |
| 234 | +10. ✅ Test with fresh NotePlan restart (clear any cached state) |
| 235 | +
|
| 236 | +## Testing Strategy |
| 237 | +
|
| 238 | +### Local Testing |
| 239 | +```javascript |
| 240 | +// Add diagnostic command to plugin: |
| 241 | +export async function testRequestHandlers(): Promise<void> { |
| 242 | + const handlers = [ |
| 243 | + { name: 'getFavoriteNotes', params: {} }, |
| 244 | + { name: 'getFavoriteCommands', params: {} }, |
| 245 | + // ... more handlers |
| 246 | + ] |
| 247 | + |
| 248 | + for (const { name, params } of handlers) { |
| 249 | + const startTime = Date.now() |
| 250 | + console.log(`Testing ${name}...`) |
| 251 | + try { |
| 252 | + const result = await handleRequest(name, params) |
| 253 | + const elapsed = Date.now() - startTime |
| 254 | + console.log(`✅ ${name}: success=${result.success}, elapsed=${elapsed}ms`) |
| 255 | + } catch (error) { |
| 256 | + const elapsed = Date.now() - startTime |
| 257 | + console.log(`❌ ${name}: error="${error.message}", elapsed=${elapsed}ms`) |
| 258 | + } |
| 259 | + } |
| 260 | +} |
| 261 | +``` |
| 262 | +
|
| 263 | +### Production Debugging |
| 264 | +
|
| 265 | +Add comprehensive logging: |
| 266 | +```javascript |
| 267 | +// In router: |
| 268 | +export const onYourRouter = newCommsRouter({ |
| 269 | + routerName: 'YourRouter/DEBUG', // Add DEBUG suffix for extra visibility |
| 270 | + defaultWindowId: YOUR_WINDOW_ID, |
| 271 | + routeRequest: async (actionType: string, data: any): Promise<RequestResponse> => { |
| 272 | + logDebug(pluginJson, `[ROUTER] Received request: actionType="${actionType}", correlationId="${data.__correlationId}"`) |
| 273 | + const result = await routeYourRequest(actionType, data) |
| 274 | + logDebug(pluginJson, `[ROUTER] Sending response: success=${result.success}, correlationId="${data.__correlationId}"`) |
| 275 | + return result |
| 276 | + }, |
| 277 | + pluginJson: pluginJson, |
| 278 | +}) |
| 279 | +``` |
| 280 | +
|
| 281 | +## Performance Monitoring |
| 282 | +
|
| 283 | +Track request timing: |
| 284 | +```javascript |
| 285 | +// In handler: |
| 286 | +export async function handleGetData(requestData: Object): Promise<RequestResponse> { |
| 287 | + const perfMarks = { |
| 288 | + start: Date.now(), |
| 289 | + dataFetch: 0, |
| 290 | + processing: 0, |
| 291 | + complete: 0, |
| 292 | + } |
| 293 | + |
| 294 | + try { |
| 295 | + // Fetch data |
| 296 | + const data = await fetchData() |
| 297 | + perfMarks.dataFetch = Date.now() |
| 298 | + |
| 299 | + // Process data |
| 300 | + const processed = processData(data) |
| 301 | + perfMarks.processing = Date.now() |
| 302 | + |
| 303 | + perfMarks.complete = Date.now() |
| 304 | + |
| 305 | + logDebug(pluginJson, |
| 306 | + `handleGetData: PERF: ` + |
| 307 | + `fetch=${perfMarks.dataFetch - perfMarks.start}ms, ` + |
| 308 | + `process=${perfMarks.processing - perfMarks.dataFetch}ms, ` + |
| 309 | + `total=${perfMarks.complete - perfMarks.start}ms` |
| 310 | + ) |
| 311 | + |
| 312 | + return { success: true, data: processed } |
| 313 | + } catch (error) { |
| 314 | + logError(pluginJson, `handleGetData: ERROR after ${Date.now() - perfMarks.start}ms: ${error.message}`) |
| 315 | + return { success: false, message: error.message } |
| 316 | + } |
| 317 | +} |
| 318 | +``` |
| 319 | +
|
| 320 | +## See Also |
| 321 | +
|
| 322 | +- [ROUTER_AND_HANDLERS.md](../../dwertheimer.ReactSkeleton/ROUTER_AND_HANDLERS.md) - Router organization guide |
| 323 | +- [COMMUNICATION_STRATEGY.md](../../dwertheimer.Forms/COMMUNICATION_STRATEGY.md) - Request/response pattern details |
| 324 | +- `@helpers/react/routerUtils.js` - Shared router utilities (ALWAYS USE THIS) |
| 325 | +- `np.Shared/src/sharedRequestRouter.js` - Example router implementation |
0 commit comments