Skip to content

Commit bc230b7

Browse files
author
davidwei
committed
Improve authentication reliability with timeouts and local validation
1 parent 3e88cb0 commit bc230b7

5 files changed

Lines changed: 121 additions & 136 deletions

File tree

CLAUDE.md

Lines changed: 0 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -23,123 +23,4 @@
2323
- **Testing**: Group related tests, use descriptive test names, mock external dependencies
2424
- **File Editing**: Use eed (Enhanced Ed) for all file modifications - NEVER use Edit/MultiEdit tools
2525

26-
## Tool Usage Policy - Critical Understanding
27-
28-
### Why Edit/MultiEdit are BANNED
29-
Edit and MultiEdit waste massive amounts of tokens and time due to fundamental design flaws for AI workflows:
30-
31-
**Problem 1: Forced Read Tax**
32-
- Edit requires you to Read entire files before modification, even when Grep already told you the exact line
33-
- Example: Change one line in a 500-line file = Read 500 lines + Edit = thousands of wasted tokens
34-
- eed: Direct modification without reading = minimal tokens
35-
36-
**Problem 2: The Try-Fail-Retry Loop**
37-
- Edit fails on multiple matches → you retry with MultiEdit → wasted round trip
38-
- This happens on almost every file with common patterns
39-
- eed: Express intent clearly with patterns (s/old/new/ or s/old/new/g) - works first try
40-
41-
**Real Cost Comparison** (modifying 5 files):
42-
- Edit path: 5 Reads (10k tokens) + 2-3 failures + 7-10 tool calls = ~12 interactions
43-
- eed path: 5 eed commands (500 tokens) + 0 failures = 5 interactions
44-
- **20x efficiency gain with eed**
45-
46-
### REQUIRED TOOL: eed (Enhanced Ed)
47-
Use eed for ALL file modifications. Fallback to sed/awk only if eed unavailable.
48-
49-
## Enhanced Ed (eed) Usage Guidelines
50-
51-
### Why eed Exists - The Real Story
52-
eed is not "just another editor" - it's a safety wrapper around ed that fixes the sharp edges:
53-
54-
**What eed Does for You Automatically**:
55-
1. **Smart operation ordering**: Converts `1a ... 5a ... 6a` to `6a→5a→1a` to prevent line number drift
56-
2. **Auto-completion**: Adds missing `w` and `q` commands so you never lose changes
57-
3. **Atomic commits**: Every edit is committed immediately with clear history
58-
4. **Safe rollback**: `eed --undo` when things go wrong
59-
60-
**Raw ed problems eed solves**:
61-
- Line numbers shift after insertions/deletions → eed reorders operations
62-
- Forgot to save → eed auto-saves
63-
- Made a mistake → eed provides undo
64-
- Need to track changes → eed commits everything
65-
66-
67-
### Why Use eed
68-
- **Atomic commits**: Every edit is automatically committed, making it safe to experiment
69-
- **Easy rollback**: `eed --undo` instantly reverts the last change
70-
- **Precision editing**: When you know line numbers, no need to re-read entire files
71-
- **Consistent workflow**: Every edit follows the same pattern with clear commit messages
72-
73-
### Learning Experience & First Steps
74-
**Start with `eed --help`** - Don't be like me and jump in directly. Understanding the tool first saves debugging time later.
75-
76-
**AI vs Human editing**: As AI, we need non-interactive, atomic edits. We can't use mouse, big screens, or make multiple changes before saving like humans. Each eed command must be complete and correct in one shot.
77-
78-
**From beginner to proficient**: My eed journey this session: started bold but clumsy → got comfortable with basic patterns → became overconfident with complex edits → made silly mistakes when tired → learned that consistency beats cleverness.
79-
80-
81-
### Quick Start - Your First eed Commands
82-
83-
**Basic syntax (commit every change)**:
84-
```bash
85-
eed -m 'what you are doing' /path/to/file <<'EOF'
86-
# ed commands here
87-
w
88-
q
89-
EOF
90-
```
91-
92-
**Most common operations**:
93-
```bash
94-
# 1. Simple substitution (first occurrence)
95-
/pattern/s/old/new/
96-
97-
# 2. Global substitution (all occurrences)
98-
s/old/new/g
99-
100-
# 3. Substitute in specific pattern context
101-
/function_name/s/old/new/
102-
103-
# 4. Delete lines matching pattern
104-
/pattern_to_delete/d
105-
106-
# 5. Append after matching line
107-
/pattern/a
108-
new line content here
109-
.
110-
111-
# 6. Change (replace) matching line
112-
/pattern/c
113-
replacement line
114-
.
115-
```
116-
117-
**When grep finds what you need**:
118-
```bash
119-
# grep tells you: "lib/foo.dart:42: some old code"
120-
# Don't Read the file! Just:
121-
eed -m 'fix the thing' lib/foo.dart <<'EOF'
122-
/old code/s/old/new/
123-
w
124-
q
125-
EOF
126-
```
127-
128-
### Practical eed Tips
129-
130-
**Pattern-based editing** (avoid line number dependency):
131-
- `/function_name/,/^}/d` - Delete entire function
132-
- `/import.*flutter/a` - Add after import section
133-
- `s/old_pattern/new_pattern/g` - Global replace
134-
135-
**Common scenarios**:
136-
- Single-line fix: Use `/pattern/s/old/new/`
137-
- Multiple files: Run parallel eed commands (no Read needed!)
138-
- Uncertain change: Make the edit, test, `eed --undo` if wrong
139-
140-
**Error recovery**:
141-
- Wrong edit → `eed --undo` immediately
142-
- Complex edit → Break into multiple simple eed commands
143-
- Preview first → Read the file ONLY if you need to understand context
144-
14526
**Key insight**: With eed, you express *intent* (what to change and where), not *exact text* (like Edit requires). This makes it robust and efficient

lib/dio_client.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ class DioClient {
1919

2020
_dio = Dio(); // Create Dio instance if not already created
2121
_dio!.options.baseUrl = AppConfig.apiBaseUrl;
22-
_dio!.options.connectTimeout = const Duration(seconds: 20);
23-
_dio!.options.receiveTimeout = const Duration(seconds: 25);
22+
// Increase timeouts for better web performance and token validation
23+
_dio!.options.connectTimeout = const Duration(seconds: 30);
24+
_dio!.options.receiveTimeout = const Duration(seconds: 35);
2425
// _dio!.options.sendTimeout = const Duration(seconds: 20);
2526

2627
_dio!.interceptors.add(LogInterceptor(requestBody: true, responseBody: true)); // Add logging interceptor

lib/providers/auth_provider.dart

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
22
import 'package:happy_notes/services/account_service.dart';
33
import 'package:happy_notes/screens/account/user_session.dart';
44
import 'package:happy_notes/dependency_injection.dart';
5+
import 'package:happy_notes/services/seq_logger.dart';
56

67
class AuthProvider with ChangeNotifier {
78
final AccountService _accountService = locator<AccountService>();
@@ -31,17 +32,41 @@ class AuthProvider with ChangeNotifier {
3132
final storedToken = await _accountService.getToken();
3233

3334
if (storedToken != null && storedToken.isNotEmpty) {
34-
// Validate token
35-
if (await _accountService.isValidToken()) {
35+
// Validate token with timeout and fallback
36+
bool isValid = false;
37+
38+
try {
39+
// Add timeout to token validation to prevent hanging
40+
isValid = await _accountService.isValidToken()
41+
.timeout(const Duration(seconds: 10));
42+
} catch (e) {
43+
// If validation times out or fails, try local validation first
44+
SeqLogger.info('Token validation timeout/error, falling back to local validation: $e');
45+
try {
46+
isValid = await _accountService.isValidTokenLocally();
47+
} catch (localError) {
48+
SeqLogger.severe('Local token validation failed: $localError');
49+
isValid = false;
50+
}
51+
}
52+
53+
if (isValid) {
3654
_token = storedToken;
37-
// Ensure session is populated
38-
await _accountService.setUserSession(token: _token);
55+
// Ensure session is populated - do this in background if it times out
56+
try {
57+
await _accountService.setUserSession(token: _token)
58+
.timeout(const Duration(seconds: 8));
59+
} catch (sessionError) {
60+
SeqLogger.info('Session setup timeout, continuing with stored token: $sessionError');
61+
// Continue anyway - user can still use the app
62+
}
3963
} else {
4064
_token = null; // Token is invalid or expired
4165
await _accountService.logout(); // Clear any stale session data
4266
}
4367
}
4468
} catch (e) {
69+
SeqLogger.severe('Failed to initialize authentication: $e');
4570
_error = 'Failed to initialize authentication: ${e.toString()}';
4671
_token = null;
4772
} finally {
@@ -117,4 +142,12 @@ class AuthProvider with ChangeNotifier {
117142

118143
/// Get current user email from session
119144
String? get currentUserEmail => UserSession().email;
145+
146+
/// Retry authentication initialization
147+
/// Useful when network conditions improve or user manually retries
148+
Future<void> retryAuth() async {
149+
_isInitialized = false;
150+
_error = null;
151+
await initAuth();
152+
}
120153
}

lib/screens/initial_page.dart

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,42 @@ class InitialPageState extends State<InitialPage> {
2020
// Show loading while AuthProvider is initializing
2121
if (!authProvider.isInitialized || authProvider.isLoading) {
2222
return const Center(
23-
child: CircularProgressIndicator(),
23+
child: Column(
24+
mainAxisAlignment: MainAxisAlignment.center,
25+
children: [
26+
CircularProgressIndicator(),
27+
SizedBox(height: 16),
28+
Text('Validating session...'),
29+
],
30+
),
31+
);
32+
}
33+
34+
// Show error if there's an authentication error
35+
if (authProvider.error != null) {
36+
return Center(
37+
child: Column(
38+
mainAxisAlignment: MainAxisAlignment.center,
39+
children: [
40+
const Icon(Icons.error_outline, size: 48, color: Colors.red),
41+
const SizedBox(height: 16),
42+
const Text('Authentication Error'),
43+
const SizedBox(height: 8),
44+
Text(
45+
authProvider.error!,
46+
style: const TextStyle(fontSize: 12),
47+
textAlign: TextAlign.center,
48+
),
49+
const SizedBox(height: 16),
50+
ElevatedButton(
51+
onPressed: () {
52+
// Retry authentication
53+
authProvider.retryAuth();
54+
},
55+
child: const Text('Retry'),
56+
),
57+
],
58+
),
2459
);
2560
}
2661

lib/services/account_service.dart

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,18 @@ class AccountService {
5050
}
5151

5252
Future<dynamic> _refreshToken() async {
53-
var apiResponse = (await _accountApi.refreshToken()).data;
54-
if (apiResponse['successful']) {
55-
_storeToken(apiResponse['data']['token']);
53+
try {
54+
var apiResponse = (await _accountApi.refreshToken()).data;
55+
if (apiResponse['successful']) {
56+
await _storeToken(apiResponse['data']['token']);
57+
_logger.i('Token refreshed successfully');
58+
} else {
59+
_logger.e('Token refresh failed: ${apiResponse['message'] ?? 'Unknown error'}');
60+
throw ApiException(apiResponse);
61+
}
62+
} catch (e) {
63+
_logger.e('Token refresh error: ${e.toString()}');
64+
rethrow;
5665
}
5766
}
5867

@@ -89,14 +98,20 @@ class AccountService {
8998
final prefs = await SharedPreferences.getInstance();
9099
final token = prefs.getString(_tokenKey);
91100
if (token != null && token != '') {
92-
var remainingTime = await _tokenUtils.getTokenRemainingTime(token);
93-
if (remainingTime.inDays <= 30) {
94-
try {
95-
//we deliberately don't use await here to avoid blocking the getToken operation
96-
_refreshToken();
97-
} catch (e) {
98-
// eat the exception
101+
try {
102+
var remainingTime = await _tokenUtils.getTokenRemainingTime(token);
103+
if (remainingTime.inDays <= 30) {
104+
try {
105+
// Add timeout to refresh token operation
106+
await _refreshToken().timeout(const Duration(seconds: 15));
107+
} catch (e) {
108+
_logger.e('Token refresh failed or timed out: ${e.toString()}');
109+
// Continue with existing token if refresh fails
110+
}
99111
}
112+
} catch (e) {
113+
_logger.e('Error checking token expiration: ${e.toString()}');
114+
// Continue with existing token if expiration check fails
100115
}
101116
}
102117
return token;
@@ -117,6 +132,26 @@ class AccountService {
117132
return false;
118133
}
119134

135+
/// Local token validation that doesn't require network calls
136+
/// Used as fallback when network validation times out
137+
Future<bool> isValidTokenLocally() async {
138+
if (await _isSameEnv()) {
139+
final token = await getToken();
140+
if (token != null && token != '') {
141+
try {
142+
// Only check token structure and expiration locally
143+
final remainingTime = await _tokenUtils.getTokenRemainingTime(token);
144+
// Give more buffer time for local validation (5 minutes instead of 1 second)
145+
return remainingTime.inMinutes >= 5;
146+
} catch (e) {
147+
_logger.e('Local token validation error: ${e.toString()}');
148+
return false;
149+
}
150+
}
151+
}
152+
return false;
153+
}
154+
120155
// if env changes, even the token is valid, we still need to ask user to log in
121156
Future<bool> _isSameEnv() async {
122157
final prefs = await SharedPreferences.getInstance();

0 commit comments

Comments
 (0)