@nkzw/fate-indexeddb 1.14.0 → 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.
Files changed (2) hide show
  1. package/README.md +155 -16
  2. 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 if available, otherwise fetches from the network.
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 references never fetch missing view fields. 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.
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` | A cache-only request needs data that isn't cached. |
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; handle pending state and errors in the calling code.
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
- The mutation call shape is the same as `react-fate`: `input`, `optimistic`, `insert`, and `view` work the same way. Vue does not have React Actions or `useActionState`, so Vue components should model pending and error state with Vue refs or your application state library.
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 return the entity type selected by the fate view.
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
- Entries that wrap their view in `list(...)` are treated as list resolvers. 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. If you omit `list(...)`, fate treats the entry as a standard query.
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.14.0",
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.14.0",
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"