@nkzw/fate-indexeddb 1.14.1 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -380,6 +380,8 @@ Vue provides the same API as a reactive resource. See [Resolving Arrays of Refs
|
|
|
380
380
|
|
|
381
381
|
We learned that `useRequest` is responsible for fetching data from the server and `useView` is used for reading data from the cache. In some situations data may not be available in the cache and `useView` might need to suspend the component to fetch only the missing data. Once that data is fetched and written to the cache, the component resumes rendering.
|
|
382
382
|
|
|
383
|
+
Let's say `PostCard` needs a post's `title` while a request for the same post's `title` and `content` is already in flight. fate can share a request by ID when it selects all the missing scalar fields with the same arguments. `PostCard` waits for that request, then fetches any fields it still needs.
|
|
384
|
+
|
|
383
385
|
_Tip: You can test this behavior in development mode with Fast Refresh (HMR) enabled in your bundler. When you edit the selection of a view, components using that view will suspend, fetch the missing data, and then resume rendering._
|
|
384
386
|
|
|
385
387
|
### Type Safety and Data Masking
|
|
@@ -600,7 +602,7 @@ Lists with different filters or sorting arguments stay separate. Loading another
|
|
|
600
602
|
|
|
601
603
|
`useRequest` supports different request modes to control caching and data freshness. The available modes are:
|
|
602
604
|
|
|
603
|
-
- `cache-first` (_default_): Returns data from the cache
|
|
605
|
+
- `cache-first` (_default_): Returns data from the cache when available and fresh. Fetches missing data and data marked stale by [list invalidation](/docs/guide/actions.md#invalidating-lists) or [record invalidation](/docs/guide/actions.md#invalidating-records).
|
|
604
606
|
- `stale-while-revalidate`: Returns data from the cache and simultaneously fetches fresh data from the network.
|
|
605
607
|
- `network-only`: Always fetches data from the network, bypassing the cache.
|
|
606
608
|
- `cache-only` (React): Observes cached data without fetching or suspending. Returns `undefined` when the selection is missing.
|
|
@@ -618,6 +620,8 @@ const { posts } = useRequest(
|
|
|
618
620
|
);
|
|
619
621
|
```
|
|
620
622
|
|
|
623
|
+
When another component mounts with the same `network-only` or `stale-while-revalidate` request, it shares a request that's still in flight or starts a fresh one.
|
|
624
|
+
|
|
621
625
|
For screens that also work offline, switch the same hook to `cache-only`:
|
|
622
626
|
|
|
623
627
|
```tsx
|
|
@@ -628,7 +632,7 @@ const result = useRequest(
|
|
|
628
632
|
const viewer = useView(ViewerView, result?.viewer);
|
|
629
633
|
```
|
|
630
634
|
|
|
631
|
-
Cache-only results stay subscribed to cache changes. Their
|
|
635
|
+
Cache-only results stay subscribed to cache changes. Their refs read selected fields, including deferred fields, from the cache. Switching offline bypasses a pending Suspense gate immediately, so the component can keep its local state and render its offline UI. The existing network modes keep their required result type; modes that can be `cache-only` return an optional result.
|
|
632
636
|
|
|
633
637
|
### Refreshing Requests
|
|
634
638
|
|
|
@@ -771,13 +775,13 @@ The snapshot contains `data`, `error`, `isFetching`, `status`, and `refetch`. Da
|
|
|
771
775
|
|
|
772
776
|
Reading a snapshot does not start a request. The first subscription starts an enabled request and retains its data; the last unsubscribe releases it. Use `enabled: false` to disable fetching, or `mode: 'cache-only'` to observe cached data without fetching missing fields. The possible statuses are:
|
|
773
777
|
|
|
774
|
-
| Status | Meaning
|
|
775
|
-
| ---------- |
|
|
776
|
-
| `disabled` | The request is not enabled.
|
|
777
|
-
| `missing` |
|
|
778
|
-
| `pending` | The initial request is loading.
|
|
779
|
-
| `ready` | Data is available.
|
|
780
|
-
| `error` | The initial request failed.
|
|
778
|
+
| Status | Meaning |
|
|
779
|
+
| ---------- | ---------------------------------------------------------------- |
|
|
780
|
+
| `disabled` | The request is not enabled. |
|
|
781
|
+
| `missing` | Data is missing after a cache-only read or a successful request. |
|
|
782
|
+
| `pending` | The initial request is loading. |
|
|
783
|
+
| `ready` | Data is available. |
|
|
784
|
+
| `error` | The initial request failed. |
|
|
781
785
|
|
|
782
786
|
`observer.getSnapshot().refetch()` returns a promise and requires an enabled request that isn't cache-only. Handle its rejected promise in the calling code.
|
|
783
787
|
|
|
@@ -1347,7 +1351,7 @@ const result = await fate.mutations.comment.add({
|
|
|
1347
1351
|
});
|
|
1348
1352
|
```
|
|
1349
1353
|
|
|
1350
|
-
The options are the same as for Actions, including `optimistic`, `insert`, and `view`. The call returns a promise
|
|
1354
|
+
The options are the same as for Actions, including `optimistic`, `insert`, `remove`, `invalidate`, and `view`. The call returns a promise. Handle pending state and errors in the calling code.
|
|
1351
1355
|
|
|
1352
1356
|
### Mutation Server Implementation
|
|
1353
1357
|
|
|
@@ -1453,6 +1457,63 @@ Insertions work with pagination, optimistic updates, and [offline mutations](/do
|
|
|
1453
1457
|
|
|
1454
1458
|
Earlier versions appended mutation results automatically. Add an `insert` option to calls that relied on that behavior. The strings `'before'` and `'after'` still enable automatic list matching, but prefer an explicit target. `'none'` is now the default.
|
|
1455
1459
|
|
|
1460
|
+
### Removing Items from Lists
|
|
1461
|
+
|
|
1462
|
+
Let's say we want to move a post from the inbox to an archive. Pass the inbox list as `remove.from` and the post's view ref as `remove.node`. Use `insert` in the same call to add the post to the archive.
|
|
1463
|
+
|
|
1464
|
+
```tsx
|
|
1465
|
+
await fate.mutations.post.archive({
|
|
1466
|
+
input: { id: postRef.id },
|
|
1467
|
+
optimistic: { archived: true },
|
|
1468
|
+
remove: { from: inbox, node: postRef },
|
|
1469
|
+
insert: { into: archivedPosts, position: 'before' },
|
|
1470
|
+
});
|
|
1471
|
+
```
|
|
1472
|
+
|
|
1473
|
+
The post disappears from the inbox immediately. If the mutation fails, fate rolls back both list changes. The post itself stays in the cache. Pass an array of removal objects to remove more than one item or update several lists.
|
|
1474
|
+
|
|
1475
|
+
```tsx
|
|
1476
|
+
await fate.mutations.post.archive({
|
|
1477
|
+
input: { id: postRef.id },
|
|
1478
|
+
remove: [
|
|
1479
|
+
{ from: inbox, node: postRef },
|
|
1480
|
+
{ from: starredPosts, node: postRef },
|
|
1481
|
+
],
|
|
1482
|
+
});
|
|
1483
|
+
```
|
|
1484
|
+
|
|
1485
|
+
Use the original array, connection, or deferred list returned by the same client. A deferred list still loads when a component reads it. If it loads while the mutation is pending, fate applies the removal to the loaded items. Removals also work with [offline mutations](/docs/guide/persistence.md).
|
|
1486
|
+
|
|
1487
|
+
If archiving the post changes which items the server would return on the first page, add the list to [`invalidate`](#invalidating-lists).
|
|
1488
|
+
|
|
1489
|
+
### Invalidating Lists
|
|
1490
|
+
|
|
1491
|
+
Archiving a post might bring the next post into the first page of the inbox. Pass the inbox list to `invalidate` to fetch the updated page on the next request.
|
|
1492
|
+
|
|
1493
|
+
```tsx
|
|
1494
|
+
await fate.mutations.post.archive({
|
|
1495
|
+
input: { id: postRef.id },
|
|
1496
|
+
invalidate: [inbox],
|
|
1497
|
+
});
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
Use the original array or connection returned by the same client. When the mutation succeeds, fate marks the list stale and keeps its current items visible. The next `cache-first` request fetches the list again. Lists with other filters keep their cached results. To refresh the list immediately, use [`useRefetch`](/docs/guide/requests.md#refreshing-requests).
|
|
1501
|
+
|
|
1502
|
+
Refreshing the first page replaces the previously loaded pages. You can load more pages from the refreshed list.
|
|
1503
|
+
|
|
1504
|
+
### Invalidating Records
|
|
1505
|
+
|
|
1506
|
+
Let's add a comment to a post. If the mutation doesn't return the post's updated comment count, pass the post's view ref to `invalidate`.
|
|
1507
|
+
|
|
1508
|
+
```tsx
|
|
1509
|
+
await fate.mutations.comment.add({
|
|
1510
|
+
input: { content, postId: postRef.id },
|
|
1511
|
+
invalidate: [postRef],
|
|
1512
|
+
});
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
When the mutation succeeds, fate marks the post's cached fields stale. A `cache-first` request refreshes the fields it selects. If it selects only the title, the comment count stays stale until a request selects it. You can pass both view refs and lists to `invalidate` in the same call.
|
|
1516
|
+
|
|
1456
1517
|
## Persistence
|
|
1457
1518
|
|
|
1458
1519
|
fate can keep data across page reloads and save mutations while a user is offline. When they come back, their views can show previously loaded data and their pending changes continue where they left off.
|
|
@@ -2019,7 +2080,7 @@ const like = async () => {
|
|
|
2019
2080
|
|
|
2020
2081
|
Expected mutation errors are returned in `result.error`; unexpected failures reject the promise. See [Action & Mutation Error Handling](/docs/guide/actions.md#action--mutation-error-handling).
|
|
2021
2082
|
|
|
2022
|
-
|
|
2083
|
+
Use Vue refs or your application state library to track pending and error state. The mutation options work the same way in both frameworks, including `optimistic`, `insert`, `remove`, `invalidate`, and `view`. See [Mutations](/docs/guide/actions.md#mutations).
|
|
2023
2084
|
|
|
2024
2085
|
### API Differences from React
|
|
2025
2086
|
|
|
@@ -2052,7 +2113,7 @@ The GraphQL transport expects a schema with Relay-style object identity and pagi
|
|
|
2052
2113
|
- Entity objects include `id` and `__typename`.
|
|
2053
2114
|
- Object fetches use `nodes(ids:)` by default, or configured per-type query fields.
|
|
2054
2115
|
- List fields return Relay connections with `edges`, `cursor`, `node`, and `pageInfo`.
|
|
2055
|
-
- Root queries and mutations
|
|
2116
|
+
- Root queries and mutations that use a fate view return objects of the selected type.
|
|
2056
2117
|
|
|
2057
2118
|
For example, a `Post` list can be exposed as a normal GraphQL connection:
|
|
2058
2119
|
|
|
@@ -2155,6 +2216,40 @@ Custom scalars default to `unknown`. Set `fateGraphQL.scalars` to describe their
|
|
|
2155
2216
|
|
|
2156
2217
|
Use `import type` for generated types. After updating the schema, run `fate generate` to refresh them.
|
|
2157
2218
|
|
|
2219
|
+
### Views on Unions and Interfaces
|
|
2220
|
+
|
|
2221
|
+
Let's say our `search` connection returns a `SearchResult` union of `Post` and `User`. We can compose `PostView` and `UserView` into a single view using `viewFor`.
|
|
2222
|
+
|
|
2223
|
+
```tsx
|
|
2224
|
+
import { viewFor, useRequest, view } from 'react-fate';
|
|
2225
|
+
import type { GraphQLEntities } from '../../.fate/client.generated.ts';
|
|
2226
|
+
import { PostCard, PostView } from './PostCard.tsx';
|
|
2227
|
+
import { UserCard, UserView } from './UserCard.tsx';
|
|
2228
|
+
|
|
2229
|
+
const SearchResultView = view<GraphQLEntities['SearchResult']>()({
|
|
2230
|
+
...viewFor('Post', PostView),
|
|
2231
|
+
...viewFor('User', UserView),
|
|
2232
|
+
});
|
|
2233
|
+
|
|
2234
|
+
export function Search({ query }: { query: string }) {
|
|
2235
|
+
const { search } = useRequest({ search: { args: { query }, list: SearchResultView } });
|
|
2236
|
+
|
|
2237
|
+
return search.map((result) => {
|
|
2238
|
+
const key = `${result.__typename}:${result.id}`;
|
|
2239
|
+
|
|
2240
|
+
switch (result.__typename) {
|
|
2241
|
+
case 'Post':
|
|
2242
|
+
return <PostCard key={key} post={result} />;
|
|
2243
|
+
case 'User':
|
|
2244
|
+
return <UserCard key={key} user={result} />;
|
|
2245
|
+
}
|
|
2246
|
+
return null;
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
```
|
|
2250
|
+
|
|
2251
|
+
fate fetches the fields from `PostView` for posts and from `UserView` for users. Checking `__typename` gives each card a ref of the right type to resolve with `useView`. The same composition works for interfaces. Each concrete type needs an `id` so fate can identify its objects in the cache.
|
|
2252
|
+
|
|
2158
2253
|
### Creating a Client
|
|
2159
2254
|
|
|
2160
2255
|
Create the client with your GraphQL endpoint and provide it through the `FateClient` provider:
|
|
@@ -2344,6 +2439,16 @@ const { posts } = useRequest({
|
|
|
2344
2439
|
|
|
2345
2440
|
This produces a root `posts(first: 10)` field and a nested `comments(first: 3)` field in the generated GraphQL selection.
|
|
2346
2441
|
|
|
2442
|
+
#### Requesting Scalars
|
|
2443
|
+
|
|
2444
|
+
Let's say the schema defines `today: Date!` with a custom `Date` scalar. Add `today: { embedded: true, type: 'Date' }` to `fateGraphQL.roots`, then use `value: true` to request it.
|
|
2445
|
+
|
|
2446
|
+
```tsx
|
|
2447
|
+
const { today } = useRequest({ today: { value: true } });
|
|
2448
|
+
```
|
|
2449
|
+
|
|
2450
|
+
fate returns the value as sent by the server. This also applies to custom scalars that contain objects or arrays, such as `JSON`.
|
|
2451
|
+
|
|
2347
2452
|
### Mutations
|
|
2348
2453
|
|
|
2349
2454
|
Map fate mutation names to GraphQL mutation fields:
|
|
@@ -2428,6 +2533,8 @@ const fate = createFateClient({
|
|
|
2428
2533
|
|
|
2429
2534
|
Your server must implement these subscription fields. See the [GraphQL subscription example](https://github.com/nkzw-tech/fate/blob/main/example/server-graphql/src/graphql/live.tsx) for their event shapes and resolvers. For servers without these subscriptions, set `live: false`.
|
|
2430
2535
|
|
|
2536
|
+
For a list with several entity types, fate subscribes to membership events with an empty `select`. Return an event with `type: 'invalidate'` when membership changes. fate reruns the list query with the original arguments and view selection.
|
|
2537
|
+
|
|
2431
2538
|
### Existing Servers
|
|
2432
2539
|
|
|
2433
2540
|
Your GraphQL server continues to handle authorization, validation, pagination, and mutation side effects. You don't need fate's server or ORM adapters.
|
|
@@ -2586,7 +2693,7 @@ export const Root = {
|
|
|
2586
2693
|
};
|
|
2587
2694
|
```
|
|
2588
2695
|
|
|
2589
|
-
|
|
2696
|
+
Use `list(...)` for a list resolver and a plain data view for a standard query. In the native protocol, the root key is the operation name used by the client. In the tRPC adapter, `procedure` can point that root at a specific router procedure.
|
|
2590
2697
|
|
|
2591
2698
|
Each `list(...)` can have its own ordering. For example, the feed can show newest posts first while comments use oldest first. Without `orderBy`, fate sorts by `id asc`. It also adds `id asc` as a tie-breaker when your ordering doesn't include `id`. Use an array when ordering by several fields.
|
|
2592
2699
|
|
|
@@ -2609,6 +2716,38 @@ const { posts, categories, viewer } = useRequest({
|
|
|
2609
2716
|
});
|
|
2610
2717
|
```
|
|
2611
2718
|
|
|
2719
|
+
#### Lists with Several Types
|
|
2720
|
+
|
|
2721
|
+
Let's add a `search` list that returns both posts and users. Pass a data view for each type to `list`, along with a query that finds the results for each page. The data view keys match the type names, and each view selects `id`. The `typeName` gives the combined type its own name in the generated client.
|
|
2722
|
+
|
|
2723
|
+
Mixed-list membership grants access through the list query. Register an authorized standalone read for members that need direct by-ID lookups, including deferred reads.
|
|
2724
|
+
|
|
2725
|
+
```tsx
|
|
2726
|
+
import { list } from '@nkzw/fate/server';
|
|
2727
|
+
|
|
2728
|
+
export const Root = {
|
|
2729
|
+
search: list(
|
|
2730
|
+
{ Post: postDataView, User: userDataView },
|
|
2731
|
+
{
|
|
2732
|
+
query: querySearch,
|
|
2733
|
+
typeName: 'SearchResult',
|
|
2734
|
+
},
|
|
2735
|
+
),
|
|
2736
|
+
};
|
|
2737
|
+
```
|
|
2738
|
+
|
|
2739
|
+
Your `querySearch` function receives `args`, `ctx`, `cursor`, `direction`, and `take`. Return up to `take` entries in display order, starting after the cursor for forward pagination or ending before it for backward pagination. Each entry has a `__typename`, a string `id`, and a unique nonempty string `cursor`. Include each `(__typename, id)` pair once across the results. `take` includes one extra entry so fate can tell whether there's another page. fate fetches the selected fields in batches for each type and applies the permissions from its data view.
|
|
2740
|
+
|
|
2741
|
+
For native HTTP, pass `Root` to `createFateServer`. For tRPC, use `fate.procedures` and register the router as `searchResult` in `appRouter`.
|
|
2742
|
+
|
|
2743
|
+
```tsx
|
|
2744
|
+
const searchResultRouter = router(fate.procedures({ view: Root.search }));
|
|
2745
|
+
```
|
|
2746
|
+
|
|
2747
|
+
To use a different router name, set `router` on the entry in `Root`, for example `search: { router: 'search', view: searchList }`. On the client, use [`viewFor` to compose the views](/docs/integrations/graphql.md#views-on-unions-and-interfaces) for the search results.
|
|
2748
|
+
|
|
2749
|
+
Live membership events refresh mixed lists through the same scoped query. This applies the subscriber’s access rules and reloads the fields selected by each view.
|
|
2750
|
+
|
|
2612
2751
|
### Native fate protocol
|
|
2613
2752
|
|
|
2614
2753
|
Create a source adapter for your ORM, pass it to `createFateServer`, and add a route for the HTTP handler:
|
|
@@ -3248,13 +3387,13 @@ Your app can publish live updates through the normal fate live bus:
|
|
|
3248
3387
|
|
|
3249
3388
|
```tsx
|
|
3250
3389
|
live.update('Post', postId, { changed: ['likes'] });
|
|
3251
|
-
live.connection('Post.comments', { id: postId }).appendNode('Comment', commentId
|
|
3252
|
-
node: comment,
|
|
3253
|
-
});
|
|
3390
|
+
live.connection('Post.comments', { id: postId }).appendNode('Comment', commentId);
|
|
3254
3391
|
```
|
|
3255
3392
|
|
|
3256
3393
|
Pass `changed` to refresh only those fields, or omit it to refresh the whole selected view.
|
|
3257
3394
|
|
|
3395
|
+
Void sends IDs and change metadata with live events. Each subscriber fetches the selected fields using its own credentials.
|
|
3396
|
+
|
|
3258
3397
|
### Routes
|
|
3259
3398
|
|
|
3260
3399
|
Define a live stream once in a server-only module:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nkzw/fate-indexeddb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "Optional IndexedDB persistence adapter for fate.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"idb": "^8.0.4"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@nkzw/fate": "1.
|
|
22
|
+
"@nkzw/fate": "1.15.0",
|
|
23
23
|
"fake-indexeddb": "^6.2.7",
|
|
24
24
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.5.0",
|
|
25
25
|
"vite-plus": "1.5.0"
|