Skip to content

Commit 07320a8

Browse files
vladjercaseferturan
authored andcommitted
docs(requests): document offline-aware tracking mutation pattern
Pattern 5 covers the offline action queue: route tracking writes through executeOrEnqueue, overlay read stores with findPendingOverride, and the checklist for registering a new offline-queueable endpoint. Cross-linked from the use* hooks section in components.md.
1 parent ca09b67 commit 07320a8

2 files changed

Lines changed: 122 additions & 25 deletions

File tree

.agents/rules/components.md

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,10 @@ export function useUser() {
221221
- Return `$derived` values, not raw observables
222222
- Do **not** import across feature boundaries - if sharing needed, expose via
223223
context or shared section
224+
- Hooks that fire user tracking mutations (history, watchlist, ratings,
225+
favorites) route writes through `executeOrEnqueue` and overlay reads with
226+
`findPendingOverride` so actions work offline - see "Pattern 5" in
227+
`requests.md`
224228

225229
### Hooks that drive `useQuery` take `Observable<T>`, never a bare value
226230

@@ -388,8 +392,12 @@ concatenation.
388392
```
389393

390394
```scss
391-
button[data-variant='primary'] { … }
392-
button[data-style='filled'] { … }
395+
button[data-variant='primary'] {
396+
397+
}
398+
button[data-style='filled'] {
399+
400+
}
393401
```
394402

395403
Common attributes:
@@ -451,8 +459,12 @@ nests them under the root selector so the global namespace stays clean.
451459

452460
```scss
453461
.trakt-card {
454-
.card-cover { … }
455-
.card-title { … }
462+
.card-cover {
463+
464+
}
465+
.card-title {
466+
467+
}
456468
}
457469
```
458470

@@ -478,9 +490,15 @@ expressed as state classes prefixed `is-` or `has-`, toggled with Svelte's
478490

479491
```scss
480492
.trakt-drawer {
481-
&.is-open { … }
482-
&.is-loading { … }
483-
&.has-error { … }
493+
&.is-open {
494+
495+
}
496+
&.is-loading {
497+
498+
}
499+
&.has-error {
500+
501+
}
484502
}
485503
```
486504

@@ -504,13 +522,23 @@ components and are forbidden.
504522

505523
```scss
506524
// Good
507-
.trakt-button { … }
508-
.trakt-button[data-variant='primary'] { … }
509-
.trakt-button .button-label { … }
525+
.trakt-button {
526+
527+
}
528+
.trakt-button[data-variant='primary'] {
529+
530+
}
531+
.trakt-button .button-label {
532+
533+
}
510534

511535
// Bad
512-
button { … } // leaks globally
513-
.primary { … } // collides with utility
536+
button {
537+
538+
} // leaks globally
539+
.primary {
540+
541+
} // collides with utility
514542
.button[data-variant='primary'] // unprefixed, fragile
515543
```
516544

.agents/rules/requests.md

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ applyTo: 'projects/client/src/lib/requests/**'
1111

1212
`lib/requests/` integrates with two sources:
1313

14-
- **`@trakt/api`** - Typed SDK for Trakt v2 REST API. Use `api({ fetch })` to call it.
15-
- **`v3/` endpoints** - Untyped internal endpoints accessed via `rawApiFetch`. Always define a local Zod schema for their response.
14+
- **`@trakt/api`** - Typed SDK for Trakt v2 REST API. Use `api({ fetch })` to
15+
call it.
16+
- **`v3/` endpoints** - Untyped internal endpoints accessed via `rawApiFetch`.
17+
Always define a local Zod schema for their response.
1618

1719
---
1820

@@ -69,8 +71,10 @@ export const someQuery = defineQuery({
6971
- `request` receives full `params` object - destructure only what you need.
7072
- `mapper` transforms `response.body` (typed by SDK) into domain model.
7173
- `schema` is Zod schema for **output** - validates what `mapper` returns.
72-
- `dependencies` must list every param that, when changed, should refetch. Use spread helpers for filters/search (see below).
73-
- `invalidations` lists `InvalidateAction.*` tokens that bust this cache when mutations fire.
74+
- `dependencies` must list every param that, when changed, should refetch. Use
75+
spread helpers for filters/search (see below).
76+
- `invalidations` lists `InvalidateAction.*` tokens that bust this cache when
77+
mutations fire.
7478

7579
---
7680

@@ -123,7 +127,9 @@ export const someListQuery = defineInfiniteQuery({
123127

124128
## Pattern 3 - `v3/` Endpoint Query
125129

126-
`v3/` endpoints not covered by `@trakt/api`'s type system. **Always** define a Zod schema locally for their response, parse with it before returning from request function.
130+
`v3/` endpoints not covered by `@trakt/api`'s type system. **Always** define a
131+
Zod schema locally for their response, parse with it before returning from
132+
request function.
127133

128134
```ts
129135
import { defineQuery } from '$lib/features/query/defineQuery.ts';
@@ -174,15 +180,19 @@ export const someV3Query = defineQuery({
174180
### Key rules
175181

176182
- **Always** `.parse()` raw JSON - never trust unvalidated `v3/` responses.
177-
- If endpoint can return empty/error body, guard with `response.ok` and return `{ body: undefined, status: 200 }` as fallback so `mapper` receives `undefined` and handles it gracefully.
183+
- If endpoint can return empty/error body, guard with `response.ok` and return
184+
`{ body: undefined, status: 200 }` as fallback so `mapper` receives
185+
`undefined` and handles it gracefully.
178186
- Export `ResponseSchema` when other files need to reference it.
179-
- Keep raw response schema (`...ResponseSchema`) and domain schema (`...Schema`) separate.
187+
- Keep raw response schema (`...ResponseSchema`) and domain schema (`...Schema`)
188+
separate.
180189

181190
---
182191

183192
## Pattern 4 - Mutation Request
184193

185-
Use for write operations (add, remove, update). Plain async functions - not queries.
194+
Use for write operations (add, remove, update). Plain async functions - not
195+
queries.
186196

187197
```ts
188198
import { api, type ApiParams } from '$lib/requests/api.ts';
@@ -202,16 +212,71 @@ export function someActionRequest(
202212
}
203213
```
204214

205-
For `v3/` mutations use `rawApiFetch` with `method: 'POST'` / `'DELETE'` and validate with Zod if response body matters.
215+
For `v3/` mutations use `rawApiFetch` with `method: 'POST'` / `'DELETE'` and
216+
validate with Zod if response body matters.
217+
218+
---
219+
220+
## Pattern 5 - Offline-Aware Tracking Mutation
221+
222+
User tracking mutations (history, watchlist, ratings, favorites) must survive a
223+
dead connection: they queue in IndexedDB and replay when connectivity returns.
224+
The infrastructure lives in `lib/features/offline/`.
225+
226+
**Hooks never call these request functions directly.** They go through
227+
`executeOrEnqueue`, which runs the request when online and queues it (deduped by
228+
media key set, latest intent wins) on network failure:
229+
230+
```ts
231+
import { executeOrEnqueue } from '$lib/features/offline/executeOrEnqueue.ts';
232+
import { toMediaKey } from '$lib/features/offline/toMediaKey.ts';
233+
234+
await executeOrEnqueue({
235+
endpoint: 'watchlist:add',
236+
keys: media.map((item) => toMediaKey(type, item.id)),
237+
body,
238+
invalidations: [InvalidateAction.Watchlisted(type)],
239+
});
240+
await invalidate(InvalidateAction.Watchlisted(type));
241+
```
242+
243+
The matching read store overlays the pending queue so the UI reflects the action
244+
while offline (see `useIsWatched` / `useIsWatchlisted`):
245+
246+
```ts
247+
const pending = findPendingOverride({ actions: $actions, domain, keys });
248+
if (pending) return isAddEndpoint(pending.endpoint);
249+
// ...fall through to server state
250+
```
251+
252+
### Adding a new offline-queueable action
253+
254+
1. Add the endpoint to `OfflineActionEndpointSchema`
255+
(`offline/models/OfflineActionEndpoint.ts`) - format `{domain}:{add|remove}`.
256+
2. Map its body type in `offline/models/OfflineActionBody.ts`.
257+
3. Register the executor in `offline/_internal/executeOfflineAction.ts`
258+
(endpoint -> existing `*Request` function).
259+
4. Route the hook's write through `executeOrEnqueue` with `toMediaKey` keys and
260+
the same `InvalidateAction` tokens the hook already fires.
261+
5. Overlay the read store with `findPendingOverride` + `isAddEndpoint`.
262+
6. If replaying the action can duplicate server-side effects (like extra history
263+
plays), extend the reconcile step
264+
(`offline/_internal/findDuplicateWatch.ts`); idempotent actions (watchlist,
265+
ratings, favorites) skip it.
266+
267+
Keep real-time actions (e.g. check-in) out of the queue - replaying them after
268+
the fact is semantically wrong.
206269

207270
---
208271

209272
## Mapper Functions
210273

211274
- Pure functions - no side effects, no API calls.
212275
- Named `mapTo{DomainType}(apiResponse): DomainType`.
213-
- Live in `_internal/` if reused across queries; inline or exported from query file if used only once or twice.
214-
- When extending existing mapper (e.g., adding a field), prefer `.extend()` on schema rather than duplicating.
276+
- Live in `_internal/` if reused across queries; inline or exported from query
277+
file if used only once or twice.
278+
- When extending existing mapper (e.g., adding a field), prefer `.extend()` on
279+
schema rather than duplicating.
215280

216281
```ts
217282
// _internal/mapToEntry.ts
@@ -229,7 +294,8 @@ export function mapToEntry(response: EntryResponse): Entry {
229294
## Models
230295

231296
- Define Zod schema first; derive TypeScript type with `z.infer`.
232-
- Use `.nullish()` for optional nullable fields, `.optional()` for fields that may be absent.
297+
- Use `.nullish()` for optional nullable fields, `.optional()` for fields that
298+
may be absent.
233299
- Export both schema (`EntitySchema`) and type (`Entity`) from same file.
234300

235301
```ts
@@ -281,7 +347,8 @@ Leave `invalidations: []` for queries that never need external cache busting.
281347

282348
## Filter & Search Dependencies
283349

284-
When a query accepts `FilterParams` or `SearchParams`, spread the helper functions in `dependencies`:
350+
When a query accepts `FilterParams` or `SearchParams`, spread the helper
351+
functions in `dependencies`:
285352

286353
```ts
287354
import { getGlobalFilterDependencies } from '$lib/requests/_internal/getGlobalFilterDependencies.ts';
@@ -335,3 +402,5 @@ Before submitting a new query or request, verify:
335402
- [ ] `invalidations` lists relevant `InvalidateAction.*` tokens
336403
- [ ] Mapper is pure function - no side effects
337404
- [ ] `ttl` is appropriate for data's freshness requirements
405+
- [ ] Tracking mutation: hook routes through `executeOrEnqueue` (Pattern 5),
406+
read store overlays the pending queue via `findPendingOverride`

0 commit comments

Comments
 (0)