docs: update passcode security status and add async $effect guard pattern
- TODO__Agents.md: check off completed passcode JWT migration items; document the three remaining cleanup steps (deferred ~a few days) - PROJECT__AE_Site_Passcode_Security.md: update status to active/cleanup-deferred, check off completed implementation checklist items - GUIDE__SvelteKit2_Svelte5_DexieJS.md: add new section documenting the async-function-from-$effect guard-reset infinite loop pattern, with the real example from the passcode auth bug (2026-06-18) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -558,6 +558,78 @@ Before wrapping a store read in `untrack()`, ask: **"Do I need this effect to re
|
||||
|
||||
---
|
||||
|
||||
## Async Functions Called from `$effect`: Guard-Reset Infinite Loop
|
||||
|
||||
When an `$effect` uses a guard variable to prevent re-running (e.g., `checked_passcode = entered_passcode`), and then calls an async function **without `await`**, any `$state` write that async function makes after its internal `await` will re-trigger the calling `$effect`. If it resets the guard, the loop is infinite.
|
||||
|
||||
### How It Happens
|
||||
|
||||
```typescript
|
||||
// $effect sets guard, then calls async function (not awaited — correct for $effect)
|
||||
$effect(() => {
|
||||
if (input && input !== checked) {
|
||||
checked = input; // ← guard set here to prevent re-trigger
|
||||
do_async_thing(); // intentionally not awaited
|
||||
}
|
||||
});
|
||||
|
||||
async function do_async_thing() {
|
||||
const resp = await fetch(...);
|
||||
if (resp.status === 401) {
|
||||
auth_error = 'Wrong.';
|
||||
checked = null; // ← BUG: resets the guard AFTER an await
|
||||
} // → effect condition is true again → loop
|
||||
}
|
||||
```
|
||||
|
||||
**Sequence of the loop:**
|
||||
1. Effect fires: `input !== null → checked = input` → calls `do_async_thing()`
|
||||
2. `do_async_thing` awaits the network call, gets 401
|
||||
3. Sets `checked = null` (a `$state` write)
|
||||
4. That write re-triggers the `$effect` — `input !== null` is still true
|
||||
5. Effect fires again → infinite 401 storm
|
||||
|
||||
### The Fix
|
||||
|
||||
**Never reset the guard from inside the async callback.** The guard was already set correctly by the effect before the call. On failure, leave it set — the user must change the input to trigger a retry.
|
||||
|
||||
Reset the guard only through a separate, input-driven effect:
|
||||
|
||||
```typescript
|
||||
$effect(() => {
|
||||
if (input && input !== checked) {
|
||||
checked = input;
|
||||
do_async_thing();
|
||||
}
|
||||
});
|
||||
|
||||
// Separate effect: reset guard only when user clears the input field
|
||||
$effect(() => {
|
||||
auth_error = null;
|
||||
if (!input) checked = null; // safe — $effect guard condition requires input to be truthy
|
||||
});
|
||||
|
||||
async function do_async_thing() {
|
||||
const resp = await fetch(...);
|
||||
if (resp.status === 401) {
|
||||
auth_error = 'Wrong.';
|
||||
// checked is NOT reset here — leave it as-is so the effect doesn't re-fire
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why the reset-on-clear is safe:** The main effect guards on `input && input !== checked`. When `input = ''`, the first condition is falsy — the effect short-circuits before the guard check, so setting `checked = null` while the input is empty can never trigger a new async call.
|
||||
|
||||
### Real Example (Passcode Auth — 2026-06-18)
|
||||
|
||||
`e_app_access_type.svelte` — the `$effect` watching `entered_passcode` called `handle_check_passcode_api()` without `await`. On 401, the function reset `checked_passcode = null`. The `$effect` fired again immediately, sending another POST, receiving another 401, looping until the tab was closed. Fix: removed `checked_passcode = null` from the 401 branch; added a dedicated input-clear effect to reset `checked_passcode` only when `entered_passcode` goes to `''`.
|
||||
|
||||
### Rule of Thumb
|
||||
|
||||
If an async function is called from inside a `$effect` (not awaited), treat every `$state` write it makes after an `await` as a potential effect re-trigger. Guard variables must only be cleared by the same reactive inputs the effect is watching — never by the async callback itself.
|
||||
|
||||
---
|
||||
|
||||
## Svelte 5 Binding Pitfalls
|
||||
|
||||
### 1. `props_invalid_value` (The "Expression Binding" Error)
|
||||
|
||||
Reference in New Issue
Block a user