Skip to content

Commit d824bb5

Browse files
committed
Fix request timeout issues in Favorites and Forms plugins
CRITICAL FIX: Removed outdated local copies of routerUtils.js that were causing silent failures when sending responses back to React components. Root Cause: - Both Favorites and Forms had local routerUtils.js copies - These were missing the required 'pluginJson' parameter - Missing parameter caused logging failures and prevented RESPONSE messages - React components would timeout after 10 seconds waiting for responses Favorites Plugin (v1.3.1): - Deleted src/routerUtils.js (outdated local copy) - Updated favoritesRouter.js to use @helpers/react/routerUtils - Added required pluginJson parameter to newCommsRouter() - Added useSharedHandlersFallback: false - Created DEBUGGING_REQUEST_TIMEOUTS.md guide Forms Plugin (v1.0.7): - Deleted src/routerUtils.js (outdated local copy) - Updated formBrowserRouter.js to use shared routerUtils - Updated formBuilderRouter.js to use shared routerUtils - Updated formSubmitRouter.js to use shared routerUtils - Added pluginJson parameter to all three routers - Added useSharedHandlersFallback: true Prevention: - Always import from @helpers/react/routerUtils - Never create local copies of shared helper files - Always pass pluginJson parameter to newCommsRouter() Fixes user-reported timeout errors that occurred unpredictably. See DEBUGGING_REQUEST_TIMEOUTS.md for comprehensive debugging guide.
1 parent 87bd1ca commit d824bb5

File tree

9 files changed

+353
-334
lines changed

9 files changed

+353
-334
lines changed

dwertheimer.Favorites/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Favorites Plugin Changelog
22

3+
## [1.3.1] - 2026-01-11 @dwertheimer
4+
5+
### Fixed
6+
- **CRITICAL**: Fixed request timeout issue by removing outdated local copy of `routerUtils.js` and switching to shared version from `@helpers/react/routerUtils`
7+
- The local copy was missing the `pluginJson` parameter required by the shared router, which could cause silent failures when sending responses back to React
8+
- Router now properly passes `pluginJson` parameter to `newCommsRouter` for correct logging and response handling
9+
310
## [1.3.0] - 2025-01-10 @dwertheimer
411

512
- Feature: Add new `/favorites-browser` command to open a sidebar window to view and open favorite notes and commands. This provides a persistent browser interface for managing and accessing your favorites.
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
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

dwertheimer.Favorites/src/favoritesRouter.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import {
1818
handleRenderMarkdown,
1919
handleGetNoteContentAsHTML,
2020
} from './requestHandlers'
21-
import { newCommsRouter, type RequestResponse } from './routerUtils'
21+
import { newCommsRouter, type RequestResponse } from '@helpers/react/routerUtils'
22+
import pluginJson from '../plugin.json'
2223

2324
const FAVORITES_BROWSER_WINDOW_ID = 'favorites-browser-window'
2425

@@ -75,4 +76,6 @@ export const onFavoritesBrowserAction: (actionType: string, data: any) => Promis
7576
routeRequest: routeFavoritesRequest,
7677
// Also handle non-REQUEST actions (like SEND_TO_PLUGIN) by routing them the same way
7778
handleNonRequestAction: routeFavoritesRequest,
79+
pluginJson: pluginJson,
80+
useSharedHandlersFallback: false, // Favorites implements all its own handlers
7881
})

0 commit comments

Comments
 (0)