{"record":{"id":"8cc39b721d2e6c5b","repo":"libnyanpasu/clash-nyanpasu","slug":"function-is-locked-fnref-current-name","errorCode":null,"errorMessage":"Function is locked: ${fnRef.current.name}","messagePattern":"Function is locked: (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"info","filePath":"frontend/nyanpasu/src/hooks/use-lock-fn.ts","lineNumber":24,"sourceCode":") => Promise<T>\n\n/**\n * Prevents concurrent execution of async functions.\n * When the function is executing, subsequent calls will be ignored until the function completes\n */\nexport function useLockFn<P extends any[] = any[], T = any>(\n  fn: LockFn<P, T>,\n): LockFn<P, T> {\n  const lockRef = useRef(false)\n  const fnRef = useRef(fn)\n\n  // Update ref on each render to ensure we have the latest fn\n  fnRef.current = fn\n\n  return useCallback(async (...args: P): Promise<T> => {\n    if (lockRef.current) {\n      // return Promise.reject(new Error(\"Function is locked\"));\n      console.warn(`Function is locked: ${fnRef.current.name}`)\n      return undefined as T\n    }\n\n    lockRef.current = true\n\n    try {\n      const result = await fnRef.current(...args)\n      return result\n    } finally {\n      lockRef.current = false\n    }\n  }, [])\n}\n","sourceCodeStart":6,"sourceCodeEnd":38,"githubUrl":"https://github.com/libnyanpasu/clash-nyanpasu/blob/f7dbce2997c633e484f54788035e770b3ee99773/frontend/nyanpasu/src/hooks/use-lock-fn.ts#L6-L38","documentation":"useLockFn wraps an async function with a lock so concurrent invocations are suppressed while one is still in flight. When a second call arrives while lockRef.current is true, the hook does not reject (the rejection path is commented out); it logs this console warning and returns undefined cast to T. The caller therefore silently gets a resolved undefined instead of the function's result.","triggerScenarios":"Any rapid double invocation of a useLockFn-wrapped callback while the first async call has not finished: double-clicking a save/cancel/clear button (handleSave, handleClear, handleCancel), double-clicking the tray icon (useTrayClickHandler), rapid language switching (setLanguage), or any execute() re-entry.","commonSituations":"Users double-click submit buttons or spam the tray icon; slow async operations (network, IPC to the backend) keep the lock held long enough that impatient second clicks are dropped. Because the return is undefined, downstream code expecting a real result may misbehave.","solutions":["Treat this as expected protective behavior — check the console for the warning to confirm a duplicate invocation was dropped, and no action is needed.","In calling code, check the result for undefined before using it: const r = await lockedFn(); if (r === undefined) return;","Reduce the window by making the wrapped async fn faster or disabling the trigger button/loading state while locked so users cannot re-click.","If you need callers to know the call was dropped, uncomment the Promise.reject path (or return a sentinel) and handle it at call sites."],"exampleFix":"// before\nconst onSave = useLockFn(async () => {\n  await save();\n  toast('saved');\n});\n// after (guard against dropped duplicate call)\nconst onSave = useLockFn(async () => {\n  await save();\n  return 'saved';\n});\nconst r = await onSave();\nif (r !== undefined) toast(r); // undefined means the call was locked/dropped","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"const result = await lockedFn();\nif (result === undefined) {\n  // previous invocation still in flight; this call was dropped\n  return;\n}","preventionTips":["Disable buttons / show a loading state while a locked async fn is running so users cannot re-trigger it.","Never rely on the return value of a useLockFn-wrapped call without checking for undefined.","Debounce high-frequency triggers (tray clicks, language switchers) before they reach the locked fn.","Keep wrapped async work short to shrink the lock window."],"tags":["react","hooks","concurrency","double-click","console-warning"],"backgroundTag":"concurrent-invocation-dropped","analyzedSha":"f7dbce2997c633e484f54788035e770b3ee99773","analyzedAt":"2026-09-08T01:24:59.197Z","contentChangedAt":"2026-09-08T01:24:59.197Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}