docs(plans): Plan-Doc auf runDebounced() ohne Parameter angleichen
Consumer-Patterns (Task 3/4) aktualisiert: $effect liest store.query
explizit und ruft runDebounced() parameterlos — matcht die live Impl
nach Commit 4edddc3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,8 +54,8 @@ export class SearchStore {
|
|||||||
|
|
||||||
constructor(opts?: SearchStoreOptions);
|
constructor(opts?: SearchStoreOptions);
|
||||||
|
|
||||||
/** Call from `$effect(() => store.runDebounced(store.query))`. Handles debounce + race-guard. */
|
/** Call from `$effect(() => { store.query; store.runDebounced(); })`. Handles debounce + race-guard. */
|
||||||
runDebounced(q: string): void;
|
runDebounced(): void;
|
||||||
/** Immediate (no debounce). Used by form `submit`. */
|
/** Immediate (no debounce). Used by form `submit`. */
|
||||||
runSearch(q: string): Promise<void>;
|
runSearch(q: string): Promise<void>;
|
||||||
/** Filter-change re-search — shorter debounce. */
|
/** Filter-change re-search — shorter debounce. */
|
||||||
@@ -124,7 +124,7 @@ describe('SearchStore', () => {
|
|||||||
const fetchImpl = mockFetch([]);
|
const fetchImpl = mockFetch([]);
|
||||||
const store = new SearchStore({ fetchImpl, debounceMs: 50 });
|
const store = new SearchStore({ fetchImpl, debounceMs: 50 });
|
||||||
store.query = 'abc';
|
store.query = 'abc';
|
||||||
store.runDebounced(store.query);
|
store.runDebounced();
|
||||||
await vi.advanceTimersByTimeAsync(100);
|
await vi.advanceTimersByTimeAsync(100);
|
||||||
expect(store.searching).toBe(false);
|
expect(store.searching).toBe(false);
|
||||||
expect(fetchImpl).not.toHaveBeenCalled();
|
expect(fetchImpl).not.toHaveBeenCalled();
|
||||||
@@ -137,7 +137,7 @@ describe('SearchStore', () => {
|
|||||||
]);
|
]);
|
||||||
const store = new SearchStore({ fetchImpl, debounceMs: 50, pageSize: 30 });
|
const store = new SearchStore({ fetchImpl, debounceMs: 50, pageSize: 30 });
|
||||||
store.query = 'pasta';
|
store.query = 'pasta';
|
||||||
store.runDebounced(store.query);
|
store.runDebounced();
|
||||||
expect(store.searching).toBe(true);
|
expect(store.searching).toBe(true);
|
||||||
await vi.advanceTimersByTimeAsync(100);
|
await vi.advanceTimersByTimeAsync(100);
|
||||||
await vi.waitFor(() => expect(fetchImpl).toHaveBeenCalled());
|
await vi.waitFor(() => expect(fetchImpl).toHaveBeenCalled());
|
||||||
@@ -155,7 +155,7 @@ describe('SearchStore', () => {
|
|||||||
]);
|
]);
|
||||||
const store = new SearchStore({ fetchImpl, debounceMs: 50 });
|
const store = new SearchStore({ fetchImpl, debounceMs: 50 });
|
||||||
store.query = 'pizza';
|
store.query = 'pizza';
|
||||||
store.runDebounced(store.query);
|
store.runDebounced();
|
||||||
await vi.advanceTimersByTimeAsync(100);
|
await vi.advanceTimersByTimeAsync(100);
|
||||||
await vi.waitFor(() => expect(store.webHits).toHaveLength(1));
|
await vi.waitFor(() => expect(store.webHits).toHaveLength(1));
|
||||||
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
||||||
@@ -170,7 +170,7 @@ describe('SearchStore', () => {
|
|||||||
]);
|
]);
|
||||||
const store = new SearchStore({ fetchImpl, debounceMs: 10 });
|
const store = new SearchStore({ fetchImpl, debounceMs: 10 });
|
||||||
store.query = 'stale-query';
|
store.query = 'stale-query';
|
||||||
store.runDebounced(store.query);
|
store.runDebounced();
|
||||||
await vi.advanceTimersByTimeAsync(15);
|
await vi.advanceTimersByTimeAsync(15);
|
||||||
store.query = 'different'; // user kept typing
|
store.query = 'different'; // user kept typing
|
||||||
await vi.waitFor(() => expect(fetchImpl).toHaveBeenCalled());
|
await vi.waitFor(() => expect(fetchImpl).toHaveBeenCalled());
|
||||||
@@ -187,7 +187,7 @@ describe('SearchStore', () => {
|
|||||||
]);
|
]);
|
||||||
const store = new SearchStore({ fetchImpl, debounceMs: 10, pageSize: 30 });
|
const store = new SearchStore({ fetchImpl, debounceMs: 10, pageSize: 30 });
|
||||||
store.query = 'meal';
|
store.query = 'meal';
|
||||||
store.runDebounced(store.query);
|
store.runDebounced();
|
||||||
await vi.advanceTimersByTimeAsync(15);
|
await vi.advanceTimersByTimeAsync(15);
|
||||||
await vi.waitFor(() => expect(store.hits).toHaveLength(30));
|
await vi.waitFor(() => expect(store.hits).toHaveLength(30));
|
||||||
expect(store.localExhausted).toBe(false);
|
expect(store.localExhausted).toBe(false);
|
||||||
@@ -209,7 +209,7 @@ describe('SearchStore', () => {
|
|||||||
]);
|
]);
|
||||||
const store = new SearchStore({ fetchImpl, debounceMs: 10, pageSize: 30 });
|
const store = new SearchStore({ fetchImpl, debounceMs: 10, pageSize: 30 });
|
||||||
store.query = 'soup';
|
store.query = 'soup';
|
||||||
store.runDebounced(store.query);
|
store.runDebounced();
|
||||||
await vi.advanceTimersByTimeAsync(15);
|
await vi.advanceTimersByTimeAsync(15);
|
||||||
await vi.waitFor(() => expect(store.hits).toHaveLength(1));
|
await vi.waitFor(() => expect(store.hits).toHaveLength(1));
|
||||||
expect(store.localExhausted).toBe(true);
|
expect(store.localExhausted).toBe(true);
|
||||||
@@ -228,7 +228,7 @@ describe('SearchStore', () => {
|
|||||||
]);
|
]);
|
||||||
const store = new SearchStore({ fetchImpl, debounceMs: 10 });
|
const store = new SearchStore({ fetchImpl, debounceMs: 10 });
|
||||||
store.query = 'anything';
|
store.query = 'anything';
|
||||||
store.runDebounced(store.query);
|
store.runDebounced();
|
||||||
await vi.advanceTimersByTimeAsync(15);
|
await vi.advanceTimersByTimeAsync(15);
|
||||||
await vi.waitFor(() => expect(store.webError).toBe('SearXNG unreachable'));
|
await vi.waitFor(() => expect(store.webError).toBe('SearXNG unreachable'));
|
||||||
expect(store.webExhausted).toBe(true);
|
expect(store.webExhausted).toBe(true);
|
||||||
@@ -239,7 +239,7 @@ describe('SearchStore', () => {
|
|||||||
const fetchImpl = mockFetch([]);
|
const fetchImpl = mockFetch([]);
|
||||||
const store = new SearchStore({ fetchImpl, debounceMs: 100 });
|
const store = new SearchStore({ fetchImpl, debounceMs: 100 });
|
||||||
store.query = 'foobar';
|
store.query = 'foobar';
|
||||||
store.runDebounced(store.query);
|
store.runDebounced();
|
||||||
store.reset();
|
store.reset();
|
||||||
await vi.advanceTimersByTimeAsync(200);
|
await vi.advanceTimersByTimeAsync(200);
|
||||||
expect(store.query).toBe('');
|
expect(store.query).toBe('');
|
||||||
@@ -264,7 +264,7 @@ describe('SearchStore', () => {
|
|||||||
store.restoreSnapshot(snap);
|
store.restoreSnapshot(snap);
|
||||||
expect(store.query).toBe('lasagne');
|
expect(store.query).toBe('lasagne');
|
||||||
expect(store.hits).toHaveLength(1);
|
expect(store.hits).toHaveLength(1);
|
||||||
store.runDebounced(store.query); // should NOT re-fetch after restore
|
store.runDebounced(); // should NOT re-fetch after restore
|
||||||
await vi.advanceTimersByTimeAsync(100);
|
await vi.advanceTimersByTimeAsync(100);
|
||||||
expect(fetchImpl).not.toHaveBeenCalled();
|
expect(fetchImpl).not.toHaveBeenCalled();
|
||||||
const round = store.captureSnapshot();
|
const round = store.captureSnapshot();
|
||||||
@@ -283,7 +283,7 @@ describe('SearchStore', () => {
|
|||||||
filterParam: () => '&domains=chefkoch.de'
|
filterParam: () => '&domains=chefkoch.de'
|
||||||
});
|
});
|
||||||
store.query = 'curry';
|
store.query = 'curry';
|
||||||
store.runDebounced(store.query);
|
store.runDebounced();
|
||||||
await vi.advanceTimersByTimeAsync(15);
|
await vi.advanceTimersByTimeAsync(15);
|
||||||
await vi.waitFor(() => expect(fetchImpl).toHaveBeenCalledTimes(2));
|
await vi.waitFor(() => expect(fetchImpl).toHaveBeenCalledTimes(2));
|
||||||
expect(fetchImpl.mock.calls[0][0]).toMatch(/&domains=chefkoch\.de/);
|
expect(fetchImpl.mock.calls[0][0]).toMatch(/&domains=chefkoch\.de/);
|
||||||
@@ -305,7 +305,7 @@ describe('SearchStore', () => {
|
|||||||
filterParam: () => filter
|
filterParam: () => filter
|
||||||
});
|
});
|
||||||
store.query = 'broth';
|
store.query = 'broth';
|
||||||
store.runDebounced(store.query);
|
store.runDebounced();
|
||||||
await vi.advanceTimersByTimeAsync(15);
|
await vi.advanceTimersByTimeAsync(15);
|
||||||
// Simulate filter change
|
// Simulate filter change
|
||||||
filter = '&domains=chefkoch.de';
|
filter = '&domains=chefkoch.de';
|
||||||
@@ -400,10 +400,11 @@ export class SearchStore {
|
|||||||
Add to the class:
|
Add to the class:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
runDebounced(_q: string): void {
|
runDebounced(): void {
|
||||||
// Parameter is present so consumers can wire `$effect(() => store.runDebounced(store.query))`
|
// Consumer pattern:
|
||||||
// — the `$effect` needs to read `store.query` to track it. We re-read this.query here
|
// $effect(() => { store.query; store.runDebounced(); });
|
||||||
// to stay consistent with the actual live value.
|
// The bare `store.query` read registers the reactive dep; this method
|
||||||
|
// then reads `this.query` live to kick off / debounce the search.
|
||||||
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
||||||
if (this.skipNextDebounce) {
|
if (this.skipNextDebounce) {
|
||||||
this.skipNextDebounce = false;
|
this.skipNextDebounce = false;
|
||||||
@@ -648,10 +649,11 @@ Remove the local `filterParam()` helper — the store owns it now.
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
navStore.runDebounced(navStore.query);
|
// Bare reads register the reactive deps; then kick the store.
|
||||||
|
const q = navStore.query;
|
||||||
|
navStore.runDebounced();
|
||||||
// navOpen follows query length: open while typing, close when cleared.
|
// navOpen follows query length: open while typing, close when cleared.
|
||||||
if (navStore.query.trim().length > 3) navOpen = true;
|
navOpen = q.trim().length > 3;
|
||||||
else navOpen = false;
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -795,7 +797,9 @@ Remove lines 188–199 (filter-change effect) and lines 322–347 (query-change
|
|||||||
Add:
|
Add:
|
||||||
```ts
|
```ts
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
store.runDebounced(store.query);
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
|
store.query; // register reactive dep
|
||||||
|
store.runDebounced();
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user