@nkzw/fate 0.0.8 → 0.1.1
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 +133 -73
- package/lib/cli.mjs +32 -4
- package/lib/index.d.mts +9 -4
- package/lib/index.mjs +129 -52
- package/lib/server.d.mts +1 -1
- package/lib/{types-BPmcnouE.d.mts → types-BiDLhb07.d.mts} +44 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
- **Async React:** fate uses modern Async React features like Actions, Suspense, and `use` to support concurrent rendering and enable a seamless user experience.
|
|
19
19
|
- **Lists & Pagination:** fate provides built-in support for connection-style lists with cursor-based pagination, making it easy to implement infinite scrolling and "load-more" functionality.
|
|
20
20
|
- **Optimistic Updates:** fate supports declarative optimistic updates for mutations, allowing the UI to update immediately while the server request is in-flight. If the request fails, the cache and its associated views are rolled back to their previous state.
|
|
21
|
-
- **AI-Ready:** fate's minimal, predictable API and explicit data selection enable local reasoning,
|
|
21
|
+
- **AI-Ready:** fate's minimal, predictable API and explicit data selection enable local reasoning, enabling humans and AI tools to generate stable, type-safe data-fetching code.
|
|
22
22
|
|
|
23
23
|
### A modern data client for React & tRPC
|
|
24
24
|
|
|
@@ -32,14 +32,14 @@ However, GraphQL comes with its own type system and query language. If you are a
|
|
|
32
32
|
|
|
33
33
|
Many React data frameworks lack Relay's ergonomics, especially fragment composition, co-located data requirements, predictable caching, and deep integration with modern React features. Optimistic updates usually require manually managing keys and imperative data updates, which is error-prone and tedious.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
_fate_ takes the great ideas from Relay and puts them on top of tRPC. You get the best of both worlds: type safety between the client and server, and GraphQL-like ergonomics for data fetching. Using _fate_ usually looks like this:
|
|
36
36
|
|
|
37
37
|
```tsx
|
|
38
38
|
export const PostView = view<Post>()({
|
|
39
|
+
author: UserView,
|
|
39
40
|
content: true,
|
|
40
41
|
id: true,
|
|
41
42
|
title: true,
|
|
42
|
-
author: UserView,
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
@@ -65,15 +65,15 @@ Get started with [a ready-made template](https://github.com/nkzw-tech/fate-templ
|
|
|
65
65
|
|
|
66
66
|
::: code-group
|
|
67
67
|
|
|
68
|
-
```npm
|
|
68
|
+
```bash [npm]
|
|
69
69
|
npx giget@latest gh:nkzw-tech/fate-template
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
```pnpm
|
|
72
|
+
```bash [pnpm]
|
|
73
73
|
pnpx giget@latest gh:nkzw-tech/fate-template
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
```yarn
|
|
76
|
+
```bash [yarn]
|
|
77
77
|
yarn dlx giget@latest gh:nkzw-tech/fate-template
|
|
78
78
|
```
|
|
79
79
|
|
|
@@ -87,15 +87,15 @@ yarn dlx giget@latest gh:nkzw-tech/fate-template
|
|
|
87
87
|
|
|
88
88
|
::: code-group
|
|
89
89
|
|
|
90
|
-
```npm
|
|
90
|
+
```bash [npm]
|
|
91
91
|
npm add react-fate
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
```pnpm
|
|
94
|
+
```bash [pnpm]
|
|
95
95
|
pnpm add react-fate
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
```yarn
|
|
98
|
+
```bash [yarn]
|
|
99
99
|
yarn add react-fate
|
|
100
100
|
```
|
|
101
101
|
|
|
@@ -105,15 +105,15 @@ And for your server, install the core `@nkzw/fate` package:
|
|
|
105
105
|
|
|
106
106
|
::: code-group
|
|
107
107
|
|
|
108
|
-
```npm
|
|
108
|
+
```bash [npm]
|
|
109
109
|
npm add @nkzw/fate
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
```pnpm
|
|
112
|
+
```bash [pnpm]
|
|
113
113
|
pnpm add @nkzw/fate
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
```yarn
|
|
116
|
+
```bash [yarn]
|
|
117
117
|
yarn add @nkzw/fate
|
|
118
118
|
```
|
|
119
119
|
|
|
@@ -202,34 +202,20 @@ Components using `useView` listen to changes for all selected fields. When data
|
|
|
202
202
|
|
|
203
203
|
### Fetching Data with `useRequest`
|
|
204
204
|
|
|
205
|
-
Now that we defined our view and component, we fetch the data from the server using the `useRequest` hook from fate. This hook allows us to declare what data we need for a specific screen or component tree. At the root of our
|
|
205
|
+
Now that we defined our view and component, we fetch the data from the server using the `useRequest` hook from fate. This hook allows us to declare what data we need for a specific screen or component tree. At the root of our app, we can request a list of posts like this:
|
|
206
206
|
|
|
207
207
|
```tsx
|
|
208
208
|
import { useRequest } from 'react-fate';
|
|
209
209
|
import { PostCard, PostView } from './PostCard.tsx';
|
|
210
210
|
|
|
211
|
-
export function
|
|
212
|
-
const { posts } = useRequest({
|
|
213
|
-
posts: { root: PostView, type: 'Post' },
|
|
214
|
-
} as const);
|
|
211
|
+
export function App() {
|
|
212
|
+
const { posts } = useRequest({ posts: { list: PostView } });
|
|
215
213
|
|
|
216
214
|
return posts.map((post) => <PostCard key={post.id} post={post} />);
|
|
217
215
|
}
|
|
218
216
|
```
|
|
219
217
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
```tsx
|
|
223
|
-
<ErrorBoundary FallbackComponent={ErrorComponent}>
|
|
224
|
-
<Suspense fallback={<div>Loading…</div>}>
|
|
225
|
-
<HomePage />
|
|
226
|
-
</Suspense>
|
|
227
|
-
</ErrorBoundary>
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
> [!NOTE]
|
|
231
|
-
>
|
|
232
|
-
> `useRequest` might issue multiple requests which are automatically batched together by tRPC's [HTTP Batch Link](https://trpc.io/docs/client/links/httpBatchLink).
|
|
218
|
+
_Learn more about `useRequest` in the [Requests Guide](/docs/guide/requests.md)._
|
|
233
219
|
|
|
234
220
|
### Composing Views
|
|
235
221
|
|
|
@@ -237,7 +223,7 @@ In the above example we are defining a single view for a `Post`. One of fate's c
|
|
|
237
223
|
|
|
238
224
|
```tsx
|
|
239
225
|
import { Suspense } from 'react';
|
|
240
|
-
import {
|
|
226
|
+
import { useView, ViewRef } from 'react-fate';
|
|
241
227
|
|
|
242
228
|
export const PostView = view<Post>()({
|
|
243
229
|
author: {
|
|
@@ -443,23 +429,62 @@ const PostDetail = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
|
443
429
|
|
|
444
430
|
ViewRefs carry a set of view names they can resolve. `useView` throws if a ref does not include the required view.
|
|
445
431
|
|
|
446
|
-
|
|
432
|
+
## Requests
|
|
447
433
|
|
|
448
|
-
|
|
434
|
+
### Requesting Lists
|
|
449
435
|
|
|
450
|
-
|
|
451
|
-
- `stale-while-revalidate`: Returns data from the cache and simultaneously fetches fresh data from the network.
|
|
452
|
-
- `network-only`: Always fetches data from the network, bypassing the cache.
|
|
436
|
+
The `useRequest` hook can be used to declare our data needs for a specific screen or component tree. At the root of our app, we can request a list of posts like this:
|
|
453
437
|
|
|
454
|
-
|
|
438
|
+
```tsx
|
|
439
|
+
import { useRequest } from 'react-fate';
|
|
440
|
+
import { PostCard, PostView } from './PostCard.tsx';
|
|
441
|
+
|
|
442
|
+
export function App() {
|
|
443
|
+
const { posts } = useRequest({ posts: { list: PostView } });
|
|
444
|
+
return posts.map((post) => <PostCard key={post.id} post={post} />);
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
This component suspends or throws errors, which bubble up to the nearest error boundary. Wrap your component tree with `ErrorBoundary` and `Suspense` components to show error and loading states:
|
|
455
449
|
|
|
456
450
|
```tsx
|
|
457
|
-
|
|
458
|
-
{
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
451
|
+
<ErrorBoundary FallbackComponent={ErrorComponent}>
|
|
452
|
+
<Suspense fallback={<div>Loading…</div>}>
|
|
453
|
+
<App />
|
|
454
|
+
</Suspense>
|
|
455
|
+
</ErrorBoundary>
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
> [!NOTE]
|
|
459
|
+
>
|
|
460
|
+
> `useRequest` might issue multiple requests which are automatically batched together by tRPC's [HTTP Batch Link](https://trpc.io/docs/client/links/httpBatchLink) into a single network request.
|
|
461
|
+
|
|
462
|
+
### Requesting Objects by ID
|
|
463
|
+
|
|
464
|
+
If you want to fetch data for a single object instead of a list, you can specify the `id` and the associated `view` like this:
|
|
465
|
+
|
|
466
|
+
```tsx
|
|
467
|
+
const { post } = useRequest({
|
|
468
|
+
post: { id: '12', view: PostView },
|
|
469
|
+
});
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
If you want to fetch multiple objects by their IDs, you can use the `ids` field:
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
const { posts } = useRequest({
|
|
476
|
+
posts: { ids: ['6', '7'], view: PostView },
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Other Types of Requests
|
|
481
|
+
|
|
482
|
+
For any other queries, pass only the `type` and `view`:
|
|
483
|
+
|
|
484
|
+
```tsx
|
|
485
|
+
const { viewer } = useRequest({
|
|
486
|
+
viewer: { view: UserView },
|
|
487
|
+
});
|
|
463
488
|
```
|
|
464
489
|
|
|
465
490
|
### Request Arguments
|
|
@@ -470,12 +495,32 @@ You can pass arguments to `useRequest` calls. This is useful for pagination, fil
|
|
|
470
495
|
const { posts } = useRequest({
|
|
471
496
|
posts: {
|
|
472
497
|
args: { first: 10 },
|
|
473
|
-
|
|
474
|
-
type: 'Post',
|
|
498
|
+
list: PostView,
|
|
475
499
|
},
|
|
476
500
|
});
|
|
477
501
|
```
|
|
478
502
|
|
|
503
|
+
### Request Modes
|
|
504
|
+
|
|
505
|
+
`useRequest` supports different request modes to control caching and data freshness. The available modes are:
|
|
506
|
+
|
|
507
|
+
- `cache-first` (_default_): Returns data from the cache if available, otherwise fetches from the network.
|
|
508
|
+
- `stale-while-revalidate`: Returns data from the cache and simultaneously fetches fresh data from the network.
|
|
509
|
+
- `network-only`: Always fetches data from the network, bypassing the cache.
|
|
510
|
+
|
|
511
|
+
You can pass the request mode as an option to `useRequest`:
|
|
512
|
+
|
|
513
|
+
```tsx
|
|
514
|
+
const { posts } = useRequest(
|
|
515
|
+
{
|
|
516
|
+
posts: { list: PostView },
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
mode: 'stale-while-revalidate',
|
|
520
|
+
},
|
|
521
|
+
);
|
|
522
|
+
```
|
|
523
|
+
|
|
479
524
|
## List Views
|
|
480
525
|
|
|
481
526
|
### Pagination with `useListView`
|
|
@@ -497,7 +542,7 @@ const CommentConnectionView = {
|
|
|
497
542
|
items: {
|
|
498
543
|
node: CommentView,
|
|
499
544
|
},
|
|
500
|
-
}
|
|
545
|
+
};
|
|
501
546
|
|
|
502
547
|
const PostView = view<Post>()({
|
|
503
548
|
comments: CommentConnectionView,
|
|
@@ -915,7 +960,7 @@ export const postDataView = dataView<PostItem>('Post')({
|
|
|
915
960
|
content: true,
|
|
916
961
|
id: true,
|
|
917
962
|
title: true,
|
|
918
|
-
}
|
|
963
|
+
});
|
|
919
964
|
```
|
|
920
965
|
|
|
921
966
|
### Data View Lists
|
|
@@ -950,6 +995,25 @@ export const Root = {
|
|
|
950
995
|
|
|
951
996
|
Entries that wrap their view in `list(...)` are treated as list resolvers and use the `procedure` name when calling the corresponding router procedure, defaulting to `list`. If you omit `list(...)`, fate treats the entry as a standard query and uses the view type name to infer the router name.
|
|
952
997
|
|
|
998
|
+
For the above `Root` definitions, you can make the following requests using `useRequest`:
|
|
999
|
+
|
|
1000
|
+
```tsx
|
|
1001
|
+
const query = 'Apple';
|
|
1002
|
+
|
|
1003
|
+
const { posts, categories, viewer } = useRequest({
|
|
1004
|
+
// Explicit Root queries:
|
|
1005
|
+
categories: { list: categoryView },
|
|
1006
|
+
commentSearch: { args: { query }, list: commentView },
|
|
1007
|
+
events: { list: eventView },
|
|
1008
|
+
posts: { list: postView },
|
|
1009
|
+
viewer: { view: userView },
|
|
1010
|
+
|
|
1011
|
+
// Queries by id, if those entities have a `byId` query defined:
|
|
1012
|
+
post: { id: '12', view: postView },
|
|
1013
|
+
comment: { ids: ['6', '7'], view: commentView },
|
|
1014
|
+
});
|
|
1015
|
+
```
|
|
1016
|
+
|
|
953
1017
|
### Data View Resolvers
|
|
954
1018
|
|
|
955
1019
|
fate data views support resolvers for computed fields. If we want to add a `commentCount` field to our `Post` data view, we can use the `resolver` helper that defines a Prisma selection for the database query together with a `resolve` function:
|
|
@@ -1015,34 +1079,30 @@ _Note: fate uses the specified server module name to extract the server types it
|
|
|
1015
1079
|
|
|
1016
1080
|
### Creating a _fate_ Client
|
|
1017
1081
|
|
|
1018
|
-
Now that we have generated the client types, all that remains is creating
|
|
1019
|
-
|
|
1020
|
-
Create a `fate.ts` file:
|
|
1021
|
-
|
|
1022
|
-
```tsx
|
|
1023
|
-
import { createFateClient } from './lib/fate.generated';
|
|
1024
|
-
|
|
1025
|
-
export const fate = createFateClient({
|
|
1026
|
-
links: [
|
|
1027
|
-
httpBatchLink({
|
|
1028
|
-
fetch: (input, init) =>
|
|
1029
|
-
fetch(input, {
|
|
1030
|
-
...init,
|
|
1031
|
-
credentials: 'include',
|
|
1032
|
-
}),
|
|
1033
|
-
url: `${env('SERVER_URL')}/trpc`,
|
|
1034
|
-
}),
|
|
1035
|
-
],
|
|
1036
|
-
});
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
|
-
Now wrap your app with the `FateClient` provider:
|
|
1082
|
+
Now that we have generated the client types, all that remains is creating an instance of the fate client, and using it in our React app using the `FateClient` context provider:
|
|
1040
1083
|
|
|
1041
1084
|
```tsx
|
|
1085
|
+
import { httpBatchLink } from '@trpc/client';
|
|
1042
1086
|
import { FateClient } from 'react-fate';
|
|
1043
|
-
import {
|
|
1087
|
+
import { createFateClient } from './fate.ts';
|
|
1044
1088
|
|
|
1045
1089
|
export function App() {
|
|
1090
|
+
const fate = useMemo(
|
|
1091
|
+
() =>
|
|
1092
|
+
createFateClient({
|
|
1093
|
+
links: [
|
|
1094
|
+
httpBatchLink({
|
|
1095
|
+
fetch: (input, init) =>
|
|
1096
|
+
fetch(input, {
|
|
1097
|
+
...init,
|
|
1098
|
+
credentials: 'include',
|
|
1099
|
+
}),
|
|
1100
|
+
url: `${env('SERVER_URL')}/trpc`,
|
|
1101
|
+
}),
|
|
1102
|
+
],
|
|
1103
|
+
}),
|
|
1104
|
+
[],
|
|
1105
|
+
);
|
|
1046
1106
|
return <FateClient client={fate}>{/* Components go here */}</FateClient>;
|
|
1047
1107
|
}
|
|
1048
1108
|
```
|
|
@@ -1076,14 +1136,14 @@ Probably. One day. _Maybe._
|
|
|
1076
1136
|
|
|
1077
1137
|
## Future
|
|
1078
1138
|
|
|
1079
|
-
**_fate_** is not complete yet.
|
|
1139
|
+
**_fate_** is not complete yet. The library lacks core features such as garbage collection, a compiler to extract view definitions statically ahead of time, and there is too much backend boilerplate. The current implementation of _fate_ is not tied to tRPC or Prisma, those are just the ones we are starting with. We welcome contributions and ideas to improve fate. Here are some features we'd like to add:
|
|
1080
1140
|
|
|
1081
1141
|
- Support for Drizzle
|
|
1082
1142
|
- Support backends other than tRPC
|
|
1143
|
+
- Persistent storage for offline support
|
|
1144
|
+
- Implement garbage collection for the cache
|
|
1083
1145
|
- Better code generation and less type repetition
|
|
1084
1146
|
- Support for live views and real-time updates via `useLiveView` and SSE
|
|
1085
|
-
- Implement garbage collection for the cache
|
|
1086
|
-
- Add persistent storage for offline support
|
|
1087
1147
|
|
|
1088
1148
|
## Acknowledgements
|
|
1089
1149
|
|
package/lib/cli.mjs
CHANGED
|
@@ -135,7 +135,8 @@ const generate = async () => {
|
|
|
135
135
|
if (procedureName === root$1.procedure && type === "query") queryEntries.push({
|
|
136
136
|
name: queryName,
|
|
137
137
|
procedure: root$1.procedure,
|
|
138
|
-
router
|
|
138
|
+
router,
|
|
139
|
+
type: root$1.type
|
|
139
140
|
});
|
|
140
141
|
}
|
|
141
142
|
for (const [listName, root$1] of routerRoots) {
|
|
@@ -143,7 +144,8 @@ const generate = async () => {
|
|
|
143
144
|
if (procedureName === root$1.procedure && type === "query") listEntries.push({
|
|
144
145
|
list: listName,
|
|
145
146
|
procedure: root$1.procedure,
|
|
146
|
-
router
|
|
147
|
+
router,
|
|
148
|
+
type: root$1.type
|
|
147
149
|
});
|
|
148
150
|
}
|
|
149
151
|
}
|
|
@@ -152,6 +154,24 @@ const generate = async () => {
|
|
|
152
154
|
byIdEntries.sort((a, b) => a.entityType.localeCompare(b.entityType));
|
|
153
155
|
queryEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
154
156
|
listEntries.sort((a, b) => a.list.localeCompare(b.list));
|
|
157
|
+
const rootEntries = [
|
|
158
|
+
...byIdEntries.map(({ entityType, router }) => ({
|
|
159
|
+
name: router,
|
|
160
|
+
type: entityType,
|
|
161
|
+
value: `'${router}': clientRoot<RouterOutputs['${router}']['byId'], '${entityType}'>('${entityType}'),`
|
|
162
|
+
})),
|
|
163
|
+
...queryEntries.map(({ name, procedure, router, type }) => ({
|
|
164
|
+
name,
|
|
165
|
+
type,
|
|
166
|
+
value: `'${name}': clientRoot<RouterOutputs['${router}']['${procedure}'], '${type}'>('${type}'),`
|
|
167
|
+
})),
|
|
168
|
+
...listEntries.map(({ list, procedure, router, type }) => ({
|
|
169
|
+
name: list,
|
|
170
|
+
type,
|
|
171
|
+
value: `'${list}': clientRoot<RouterOutputs['${router}']['${procedure}'], '${type}'>('${type}'),`
|
|
172
|
+
}))
|
|
173
|
+
];
|
|
174
|
+
rootEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
155
175
|
const viewTypes = Array.from(["AppRouter", ...new Set(mutationEntries.map((entry) => entry.entityType))]).sort();
|
|
156
176
|
const mutationResolverLines = mutationEntries.map(({ name, procedure, router }) => `'${name}': (client: TRPCClientType) => client.${router}.${procedure}.mutate,`);
|
|
157
177
|
const mutationConfigLines = mutationEntries.map(({ entityType, name, procedure, router }) => `'${name}': mutation<
|
|
@@ -176,6 +196,7 @@ const generate = async () => {
|
|
|
176
196
|
const mutationResolverBlock = indentBlock(mutationResolverLines.join("\n"), 4);
|
|
177
197
|
const mutationConfigBlock = indentBlock(mutationConfigLines.join("\n"), 2);
|
|
178
198
|
const byIdBlock = indentBlock(byIdLines.join("\n"), 8);
|
|
199
|
+
const rootsBlock = indentBlock(rootEntries.map((entry) => entry.value).join("\n"), 2);
|
|
179
200
|
const listsBlockContent = listLines.join("\n");
|
|
180
201
|
const listsBlock = listLines.length ? ` lists: {\n${indentBlock(listsBlockContent, 8)}\n },\n` : "";
|
|
181
202
|
const queriesBlockContent = queryLines.join("\n");
|
|
@@ -183,7 +204,7 @@ const generate = async () => {
|
|
|
183
204
|
${typeImports}
|
|
184
205
|
import { createTRPCProxyClient } from '@trpc/client';
|
|
185
206
|
import { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
|
|
186
|
-
import { createClient, createTRPCTransport, mutation } from 'react-fate';
|
|
207
|
+
import { clientRoot, createClient, createTRPCTransport, mutation } from 'react-fate';
|
|
187
208
|
|
|
188
209
|
type TRPCClientType = ReturnType<typeof createTRPCProxyClient<AppRouter>>;
|
|
189
210
|
type RouterInputs = inferRouterInputs<AppRouter>;
|
|
@@ -193,10 +214,16 @@ const mutations = {
|
|
|
193
214
|
${mutationConfigBlock}
|
|
194
215
|
} as const;
|
|
195
216
|
|
|
217
|
+
const roots = {
|
|
218
|
+
${rootsBlock}
|
|
219
|
+
} as const;
|
|
220
|
+
|
|
196
221
|
type GeneratedClientMutations = typeof mutations;
|
|
222
|
+
type GeneratedClientRoots = typeof roots;
|
|
197
223
|
|
|
198
224
|
declare module 'react-fate' {
|
|
199
225
|
interface ClientMutations extends GeneratedClientMutations {}
|
|
226
|
+
interface ClientRoots extends GeneratedClientRoots {}
|
|
200
227
|
}
|
|
201
228
|
|
|
202
229
|
export const createFateClient = (options: {
|
|
@@ -208,8 +235,9 @@ export const createFateClient = (options: {
|
|
|
208
235
|
${mutationResolverBlock}
|
|
209
236
|
} as const;
|
|
210
237
|
|
|
211
|
-
return createClient({
|
|
238
|
+
return createClient<[GeneratedClientRoots, GeneratedClientMutations]>({
|
|
212
239
|
mutations,
|
|
240
|
+
roots,
|
|
213
241
|
transport: createTRPCTransport<AppRouter, typeof trpcMutations>({
|
|
214
242
|
byId: {
|
|
215
243
|
${byIdBlock}
|
package/lib/index.d.mts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as ViewSelection, B as RequestOptions, C as TypeConfig, D as ViewEntity, E as ViewData, F as isViewTag, H as Transport, I as FateMutations, L as mutation, M as ViewTag, N as __FateEntityBrand, O as ViewEntityName, P as __FateSelectionBrand, R as FateClient, S as Snapshot, T as View, U as createTRPCTransport, V as createClient, W as getSelectionPlan, _ as Pagination, a as Entity, b as RootDefinition, c as FateThenable, d as MutationDefinition, f as MutationEntity, g as NodesItem, h as MutationResult, i as ConnectionTag, j as ViewSnapshot, k as ViewRef, l as ListItem, m as MutationInput, n as ConnectionMetadata, o as EntityId, p as MutationIdentifier, r as ConnectionRef, s as FateRoots, t as AnyRecord, u as Mask, v as Request, w as TypeName, x as Selection, y as RequestResult, z as RequestMode } from "./types-BiDLhb07.mjs";
|
|
2
2
|
|
|
3
|
-
//#region src/
|
|
3
|
+
//#region src/root.d.ts
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Defines a root query for an entity type, capturing the response shape.
|
|
7
|
+
*/
|
|
8
|
+
declare function clientRoot<Result, Type extends TypeName>(type: Type): RootDefinition<Type, Result>;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/ref.d.ts
|
|
5
11
|
/**
|
|
6
12
|
* Builds the canonical cache ID for an entity.
|
|
7
13
|
*/
|
|
8
14
|
declare const toEntityId: (type: TypeName, rawId: string | number) => EntityId;
|
|
9
15
|
//#endregion
|
|
10
16
|
//#region src/view.d.ts
|
|
11
|
-
|
|
12
17
|
type SelectionValidation<T extends Entity, S extends Selection<T>> = Exclude<keyof Omit<S, typeof __FateEntityBrand | typeof __FateSelectionBrand>, keyof Selection<T>> extends never ? unknown : never;
|
|
13
18
|
/**
|
|
14
19
|
* Creates a reusable view for an object using the declared selection.
|
|
@@ -21,4 +26,4 @@ type SelectionValidation<T extends Entity, S extends Selection<T>> = Exclude<key
|
|
|
21
26
|
*/
|
|
22
27
|
declare function view<T extends Entity>(): <S extends Selection<T>>(select: S & SelectionValidation<T, S>) => View<T, S>;
|
|
23
28
|
//#endregion
|
|
24
|
-
export { type ConnectionMetadata, type ConnectionRef, ConnectionTag, type Entity, type EntityId, FateClient, type FateMutations, type AnyRecord as FateRecord, type ListItem, type Mask, type MutationDefinition, type MutationEntity, type MutationIdentifier, type MutationInput, type MutationResult, type NodesItem, type Pagination, type Request, type RequestMode, type RequestOptions, type RequestResult, type Selection, type Snapshot, type Transport, type TypeConfig, type View, type ViewData, type ViewEntity, type ViewEntityName, type ViewRef, type ViewSelection, type ViewSnapshot, type ViewTag, createClient, createTRPCTransport, getSelectionPlan, isViewTag, mutation, toEntityId, view };
|
|
29
|
+
export { type ConnectionMetadata, type ConnectionRef, ConnectionTag, type Entity, type EntityId, FateClient, type FateMutations, type AnyRecord as FateRecord, type FateRoots, type FateThenable, type ListItem, type Mask, type MutationDefinition, type MutationEntity, type MutationIdentifier, type MutationInput, type MutationResult, type NodesItem, type Pagination, type Request, type RequestMode, type RequestOptions, type RequestResult, type Selection, type Snapshot, type Transport, type TypeConfig, type View, type ViewData, type ViewEntity, type ViewEntityName, type ViewRef, type ViewSelection, type ViewSnapshot, type ViewTag, clientRoot, createClient, createTRPCTransport, getSelectionPlan, isViewTag, mutation, toEntityId, view };
|
package/lib/index.mjs
CHANGED
|
@@ -422,6 +422,8 @@ function isNodesItem(item) {
|
|
|
422
422
|
function isQueryItem(item) {
|
|
423
423
|
return "view" in item && !("id" in item) && !("ids" in item);
|
|
424
424
|
}
|
|
425
|
+
/** Brand used on root definitions to mark their identity in the d.ts output. */
|
|
426
|
+
const RootKind = "__fate__root";
|
|
425
427
|
/** Brand used on mutation definitions to mark their identity in the d.ts output. */
|
|
426
428
|
const MutationKind = "__fate__mutation";
|
|
427
429
|
|
|
@@ -687,39 +689,45 @@ function wrapMutation(client, identifier) {
|
|
|
687
689
|
if (id$1 == null) throw new Error(`fate: Mutation '${identifier.key}' requires an 'id' to delete.`);
|
|
688
690
|
client.deleteRecord(identifier.entity, id$1, snapshots, listSnapshots);
|
|
689
691
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
result
|
|
707
|
-
};
|
|
708
|
-
} catch (error) {
|
|
709
|
-
client.clearOptimisticUpdate(optimisticToken);
|
|
710
|
-
if (snapshots.size > 0) for (const [id$2, snapshot] of snapshots) client.restore(id$2, snapshot);
|
|
711
|
-
if (listSnapshots && listSnapshots.size > 0) for (const [name, list] of listSnapshots) client.restoreList(name, list);
|
|
712
|
-
if (error instanceof Error) {
|
|
713
|
-
const { data } = error;
|
|
714
|
-
if ((data ? categorizeTRPCError(getHTTPStatusCodeFromError(data)) : "boundary") === "boundary") throw error;
|
|
692
|
+
const performMutation = async () => {
|
|
693
|
+
try {
|
|
694
|
+
const result = await client.executeMutation(identifier.key, input, selection, {
|
|
695
|
+
args,
|
|
696
|
+
plan
|
|
697
|
+
});
|
|
698
|
+
if (result && typeof result === "object" && (!deleteRecord || Boolean(view$1))) {
|
|
699
|
+
const select = collectImplicitSelectedPaths(result);
|
|
700
|
+
const pendingMask = optimisticEntityId ? client.getPendingOptimisticMask(optimisticEntityId, { excludeToken: optimisticToken }) : null;
|
|
701
|
+
const filteredSelection = optimisticEntityId ? client.filterSelectionForPendingOptimistics(optimisticEntityId, select, { excludeToken: optimisticToken }) : select;
|
|
702
|
+
client.write(identifier.entity, result, filteredSelection, void 0, plan, null, pendingMask, insert);
|
|
703
|
+
if (deleteRecord && id$1 != null) client.deleteRecord(identifier.entity, id$1);
|
|
704
|
+
const resultId = maybeGetId(config.getId, result);
|
|
705
|
+
if (optimisticEntityId && resultId != null) client.resolveOptimisticEntity(optimisticEntityId, toEntityId(identifier.entity, resultId));
|
|
706
|
+
if (optimisticRecordId != null && resultId != null && optimisticRecordId !== resultId) client.deleteRecord(identifier.entity, optimisticRecordId);
|
|
707
|
+
}
|
|
715
708
|
return {
|
|
716
|
-
error,
|
|
717
|
-
result
|
|
709
|
+
error: void 0,
|
|
710
|
+
result
|
|
718
711
|
};
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
712
|
+
} catch (error) {
|
|
713
|
+
client.clearOptimisticUpdate(optimisticToken);
|
|
714
|
+
if (snapshots.size > 0) for (const [id$2, snapshot] of snapshots) client.restore(id$2, snapshot);
|
|
715
|
+
if (listSnapshots && listSnapshots.size > 0) for (const [name, list] of listSnapshots) client.restoreList(name, list);
|
|
716
|
+
if (error instanceof Error) {
|
|
717
|
+
const { data } = error;
|
|
718
|
+
if ((data ? categorizeTRPCError(getHTTPStatusCodeFromError(data)) : "boundary") === "boundary") throw error;
|
|
719
|
+
return {
|
|
720
|
+
error,
|
|
721
|
+
result: void 0
|
|
722
|
+
};
|
|
723
|
+
} else throw new Error(`fate: Mutation '${identifier.key}' failed.`);
|
|
724
|
+
} finally {
|
|
725
|
+
client.clearOptimisticUpdate(optimisticToken);
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
const mutationPromise = performMutation();
|
|
729
|
+
if (optimisticEntityId) client.registerPendingOptimisticMutation(optimisticEntityId, mutationPromise);
|
|
730
|
+
return mutationPromise;
|
|
723
731
|
};
|
|
724
732
|
}
|
|
725
733
|
function categorizeTRPCError(statusCode) {
|
|
@@ -1006,18 +1014,18 @@ const getRequestCacheKey = (request) => {
|
|
|
1006
1014
|
const item = request[name];
|
|
1007
1015
|
if (!item) continue;
|
|
1008
1016
|
if (isNodeItem(item)) {
|
|
1009
|
-
parts.push(`node:${name}:${
|
|
1017
|
+
parts.push(`node:${name}:${getViewSignature(item.view)}:${item.id}`);
|
|
1010
1018
|
continue;
|
|
1011
1019
|
}
|
|
1012
1020
|
if (isNodesItem(item)) {
|
|
1013
|
-
parts.push(`node:${name}:${
|
|
1021
|
+
parts.push(`node:${name}:${getViewSignature(item.view)}:${item.ids.map(serializeId).join(",")}`);
|
|
1014
1022
|
continue;
|
|
1015
1023
|
}
|
|
1016
1024
|
if (isQueryItem(item)) {
|
|
1017
|
-
parts.push(`query:${name}:${
|
|
1025
|
+
parts.push(`query:${name}:${getViewSignature(item.view)}:${item.args ? hashArgs(item.args) : ""}`);
|
|
1018
1026
|
continue;
|
|
1019
1027
|
}
|
|
1020
|
-
parts.push(`list:${name}:${
|
|
1028
|
+
parts.push(`list:${name}:${getViewSignature(item.list)}:${item.args ? hashArgs(item.args) : ""}`);
|
|
1021
1029
|
}
|
|
1022
1030
|
return parts.join("$");
|
|
1023
1031
|
};
|
|
@@ -1047,22 +1055,25 @@ var FateClient = class {
|
|
|
1047
1055
|
this.parentLists = /* @__PURE__ */ new Map();
|
|
1048
1056
|
this.rootLists = /* @__PURE__ */ new Map();
|
|
1049
1057
|
this.pending = /* @__PURE__ */ new Map();
|
|
1058
|
+
this.pendingOptimisticMutations = /* @__PURE__ */ new Map();
|
|
1050
1059
|
this.optimisticMasks = /* @__PURE__ */ new Map();
|
|
1051
1060
|
this.optimisticByEntity = /* @__PURE__ */ new Map();
|
|
1061
|
+
this.optimisticEntityResolutions = /* @__PURE__ */ new Map();
|
|
1052
1062
|
this.optimisticTokenCounter = 0;
|
|
1053
1063
|
this.requests = /* @__PURE__ */ new Map();
|
|
1054
1064
|
this.rootRequests = /* @__PURE__ */ new Map();
|
|
1055
1065
|
this.stalledRequests = /* @__PURE__ */ new Set();
|
|
1056
1066
|
this.store = new Store();
|
|
1057
1067
|
this.viewDataCache = new ViewDataCache();
|
|
1068
|
+
this.actions = Object.create(null);
|
|
1069
|
+
this.mutationMap = Object.create(null);
|
|
1070
|
+
this.mutations = Object.create(null);
|
|
1071
|
+
this.roots = options.roots;
|
|
1058
1072
|
this.transport = options.transport;
|
|
1059
1073
|
this.types = new Map(options.types.map((entity) => [entity.type, {
|
|
1060
1074
|
getId,
|
|
1061
1075
|
...entity
|
|
1062
1076
|
}]));
|
|
1063
|
-
this.mutationMap = Object.create(null);
|
|
1064
|
-
this.mutations = Object.create(null);
|
|
1065
|
-
this.actions = Object.create(null);
|
|
1066
1077
|
if (options.mutations) for (const [key, definition] of Object.entries(options.mutations)) {
|
|
1067
1078
|
const mutation$1 = wrapMutation(this, {
|
|
1068
1079
|
...definition,
|
|
@@ -1174,6 +1185,11 @@ var FateClient = class {
|
|
|
1174
1185
|
}
|
|
1175
1186
|
if (type == null) throw new Error(`fate: Invalid view reference. Expected '__typename' to be provided as part of the reference, received '${JSON.stringify(ref)}'.`);
|
|
1176
1187
|
const entityId = toEntityId(type, id$1);
|
|
1188
|
+
const resolvedEntityId = this.optimisticEntityResolutions.get(entityId) ?? null;
|
|
1189
|
+
if (resolvedEntityId && resolvedEntityId !== entityId) {
|
|
1190
|
+
const { id: resolvedId, type: resolvedType } = parseEntityId(resolvedEntityId);
|
|
1191
|
+
return this.readView(view$1, createRef(resolvedType, resolvedId, view$1));
|
|
1192
|
+
}
|
|
1177
1193
|
const viewNames = getViewNames(view$1);
|
|
1178
1194
|
const refViews = ref[ViewsTag];
|
|
1179
1195
|
if (!refViews || ![...viewNames].every((name) => refViews.has(name))) {
|
|
@@ -1201,6 +1217,14 @@ var FateClient = class {
|
|
|
1201
1217
|
}
|
|
1202
1218
|
if (missing.size > 0) {
|
|
1203
1219
|
const key = this.pendingKey(entityId, missing);
|
|
1220
|
+
const pendingOptimistic = this.getPendingOptimisticMutations(entityId);
|
|
1221
|
+
if (pendingOptimistic) {
|
|
1222
|
+
const pending = this.pending.get(key);
|
|
1223
|
+
if (pending) return pending;
|
|
1224
|
+
const promise$1 = Promise.all(pendingOptimistic).then(() => this.readView(view$1, ref)).finally(() => this.pending.delete(key));
|
|
1225
|
+
this.pending.set(key, promise$1);
|
|
1226
|
+
return promise$1;
|
|
1227
|
+
}
|
|
1204
1228
|
if (this.stalledRequests.has(key)) return resolveSnapshot();
|
|
1205
1229
|
const pendingPromise = this.pending.get(key) || null;
|
|
1206
1230
|
if (pendingPromise) return pendingPromise;
|
|
@@ -1254,6 +1278,24 @@ var FateClient = class {
|
|
|
1254
1278
|
} : void 0
|
|
1255
1279
|
};
|
|
1256
1280
|
}
|
|
1281
|
+
registerPendingOptimisticMutation(entityId, promise) {
|
|
1282
|
+
let entries = this.pendingOptimisticMutations.get(entityId);
|
|
1283
|
+
if (!entries) {
|
|
1284
|
+
entries = /* @__PURE__ */ new Set();
|
|
1285
|
+
this.pendingOptimisticMutations.set(entityId, entries);
|
|
1286
|
+
}
|
|
1287
|
+
const trackedPromise = promise.catch(() => void 0);
|
|
1288
|
+
entries.add(trackedPromise);
|
|
1289
|
+
trackedPromise.finally(() => {
|
|
1290
|
+
const current = this.pendingOptimisticMutations.get(entityId);
|
|
1291
|
+
if (!current) return;
|
|
1292
|
+
current.delete(trackedPromise);
|
|
1293
|
+
if (current.size === 0) this.pendingOptimisticMutations.delete(entityId);
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
resolveOptimisticEntity(optimisticEntityId, resolvedEntityId) {
|
|
1297
|
+
this.optimisticEntityResolutions.set(optimisticEntityId, resolvedEntityId);
|
|
1298
|
+
}
|
|
1257
1299
|
registerOptimisticUpdate(entityId, select) {
|
|
1258
1300
|
if (!entityId || select.size === 0) return null;
|
|
1259
1301
|
const mask = fromPaths(select);
|
|
@@ -1295,6 +1337,10 @@ var FateClient = class {
|
|
|
1295
1337
|
}
|
|
1296
1338
|
return mask;
|
|
1297
1339
|
}
|
|
1340
|
+
getPendingOptimisticMutations(entityId) {
|
|
1341
|
+
const pending = this.pendingOptimisticMutations.get(entityId);
|
|
1342
|
+
return pending && pending.size > 0 ? [...pending] : null;
|
|
1343
|
+
}
|
|
1298
1344
|
filterSelectionForPendingOptimistics(entityId, select, options = {}) {
|
|
1299
1345
|
if (!entityId || select.size === 0) return select;
|
|
1300
1346
|
const pendingMask = this.getPendingOptimisticMask(entityId, options);
|
|
@@ -1423,30 +1469,34 @@ var FateClient = class {
|
|
|
1423
1469
|
this.executeRequest(request, { fetchAll: true }).catch(() => {});
|
|
1424
1470
|
return result;
|
|
1425
1471
|
}
|
|
1472
|
+
getRootType(name) {
|
|
1473
|
+
const root = this.roots[name];
|
|
1474
|
+
if (!root) throw new Error(`fate: Unknown root request '${name}'.`);
|
|
1475
|
+
return root.type;
|
|
1476
|
+
}
|
|
1426
1477
|
async executeRequest(request, options = {}) {
|
|
1427
1478
|
const fetchAll = options.fetchAll ?? false;
|
|
1428
1479
|
const groups = /* @__PURE__ */ new Map();
|
|
1429
1480
|
const promises = [];
|
|
1430
1481
|
for (const [name, item] of Object.entries(request)) {
|
|
1482
|
+
const type = this.getRootType(name);
|
|
1431
1483
|
const isNode = isNodeItem(item);
|
|
1432
1484
|
if (isNode || isNodesItem(item)) {
|
|
1433
1485
|
const plan = getSelectionPlan(item.view, null);
|
|
1434
1486
|
const fields = plan.paths;
|
|
1435
|
-
const
|
|
1436
|
-
const argsSignature = [...plan.args.entries()].map(([path, entry]) => `${path}:${entry.hash}`).sort().join(",");
|
|
1437
|
-
const groupKey = `${item.type}#${fieldsSignature}|${argsSignature}`;
|
|
1487
|
+
const groupKey = `${type}#${[...fields].slice().sort().join(",")}|${[...plan.args.entries()].map(([path, entry]) => `${path}:${entry.hash}`).sort().join(",")}`;
|
|
1438
1488
|
let group = groups.get(groupKey);
|
|
1439
1489
|
if (!group) {
|
|
1440
1490
|
group = {
|
|
1441
1491
|
fields,
|
|
1442
1492
|
ids: [],
|
|
1443
1493
|
plan,
|
|
1444
|
-
type
|
|
1494
|
+
type
|
|
1445
1495
|
};
|
|
1446
1496
|
groups.set(groupKey, group);
|
|
1447
1497
|
}
|
|
1448
1498
|
for (const raw of isNode ? [item.id] : item.ids) {
|
|
1449
|
-
const entityId = toEntityId(
|
|
1499
|
+
const entityId = toEntityId(type, raw);
|
|
1450
1500
|
const missing = this.store.missingForSelection(entityId, fields);
|
|
1451
1501
|
if (fetchAll || missing.size > 0) group.ids.push(raw);
|
|
1452
1502
|
}
|
|
@@ -1462,10 +1512,11 @@ var FateClient = class {
|
|
|
1462
1512
|
hasRequestData(request) {
|
|
1463
1513
|
for (const [name, item] of Object.entries(request)) {
|
|
1464
1514
|
const isNode = isNodeItem(item);
|
|
1515
|
+
const type = this.getRootType(name);
|
|
1465
1516
|
if (isNode || isNodesItem(item)) {
|
|
1466
1517
|
const fields = getSelectionPlan(item.view, null).paths;
|
|
1467
1518
|
for (const raw of isNode ? [item.id] : item.ids) {
|
|
1468
|
-
const entityId = toEntityId(
|
|
1519
|
+
const entityId = toEntityId(type, raw);
|
|
1469
1520
|
if (this.store.missingForSelection(entityId, fields).size > 0) return false;
|
|
1470
1521
|
}
|
|
1471
1522
|
continue;
|
|
@@ -1483,19 +1534,20 @@ var FateClient = class {
|
|
|
1483
1534
|
getRequestResult(request) {
|
|
1484
1535
|
const result = {};
|
|
1485
1536
|
for (const [name, item] of Object.entries(request)) {
|
|
1537
|
+
const type = this.getRootType(name);
|
|
1486
1538
|
if (isNodeItem(item)) {
|
|
1487
|
-
result[name] = this.ref(
|
|
1539
|
+
result[name] = this.ref(type, item.id, item.view);
|
|
1488
1540
|
continue;
|
|
1489
1541
|
}
|
|
1490
1542
|
if (isNodesItem(item)) {
|
|
1491
|
-
result[name] = item.ids.map((id$1) => this.ref(
|
|
1543
|
+
result[name] = item.ids.map((id$1) => this.ref(type, id$1, item.view));
|
|
1492
1544
|
continue;
|
|
1493
1545
|
}
|
|
1494
1546
|
if (isQueryItem(item)) {
|
|
1495
1547
|
const entityId = this.rootRequests.get(name);
|
|
1496
1548
|
if (entityId) {
|
|
1497
1549
|
const { id: id$1 } = parseEntityId(entityId);
|
|
1498
|
-
result[name] = createRef(
|
|
1550
|
+
result[name] = createRef(type, id$1, item.view, { root: true });
|
|
1499
1551
|
} else result[name] = null;
|
|
1500
1552
|
continue;
|
|
1501
1553
|
}
|
|
@@ -1519,7 +1571,7 @@ var FateClient = class {
|
|
|
1519
1571
|
owner: name,
|
|
1520
1572
|
procedure: `request.${name}`,
|
|
1521
1573
|
root: true,
|
|
1522
|
-
type
|
|
1574
|
+
type
|
|
1523
1575
|
};
|
|
1524
1576
|
Object.defineProperty(connection, ConnectionTag, {
|
|
1525
1577
|
configurable: false,
|
|
@@ -1543,21 +1595,22 @@ var FateClient = class {
|
|
|
1543
1595
|
if (!this.transport.fetchQuery) throw new Error(`fate: transport does not support queries. Please add support for 'fetchQuery' in your transport for '${name}'.`);
|
|
1544
1596
|
const record = await this.transport.fetchQuery(name, plan.paths, argsPayload);
|
|
1545
1597
|
if (!record || typeof record !== "object") return;
|
|
1546
|
-
const entityId = this.writeEntity(
|
|
1598
|
+
const entityId = this.writeEntity(this.getRootType(name), record, plan.paths, void 0, plan);
|
|
1547
1599
|
this.rootRequests.set(name, entityId);
|
|
1548
1600
|
}
|
|
1549
1601
|
async fetchListAndNormalize(name, item) {
|
|
1550
1602
|
if (!this.transport.fetchList) throw new Error(`fate: 'transport.fetchList' is not configured but request includes a list for key '${name}'.`);
|
|
1603
|
+
const type = this.getRootType(name);
|
|
1551
1604
|
const { argsPayload, plan } = this.resolveSelection(item.list, item.args);
|
|
1552
1605
|
const { items, pagination } = await this.transport.fetchList(name, plan.paths, argsPayload);
|
|
1553
1606
|
const ids = [];
|
|
1554
1607
|
const cursors = [];
|
|
1555
1608
|
for (const entry of items) {
|
|
1556
|
-
const id$1 = this.writeEntity(
|
|
1609
|
+
const id$1 = this.writeEntity(type, entry.node, plan.paths, void 0, plan);
|
|
1557
1610
|
ids.push(id$1);
|
|
1558
1611
|
cursors.push(entry.cursor);
|
|
1559
1612
|
}
|
|
1560
|
-
this.registerRootList(
|
|
1613
|
+
this.registerRootList(type, name);
|
|
1561
1614
|
this.store.setList(name, {
|
|
1562
1615
|
cursors,
|
|
1563
1616
|
ids,
|
|
@@ -1590,6 +1643,10 @@ var FateClient = class {
|
|
|
1590
1643
|
} else if (relationDescriptor && typeof relationDescriptor === "object" && "type" in relationDescriptor) {
|
|
1591
1644
|
if (isFieldBlocked) continue;
|
|
1592
1645
|
const childPaths = selectionTree.get(key) ?? emptySet;
|
|
1646
|
+
if (value === null) {
|
|
1647
|
+
result[key] = null;
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1593
1650
|
if (value && typeof value === "object" && !isNodeRef(value)) {
|
|
1594
1651
|
const childType = relationDescriptor.type;
|
|
1595
1652
|
const childConfig = this.types.get(childType);
|
|
@@ -1600,6 +1657,10 @@ var FateClient = class {
|
|
|
1600
1657
|
} else if (relationDescriptor && typeof relationDescriptor === "object" && "listOf" in relationDescriptor) {
|
|
1601
1658
|
if (isFieldBlocked) continue;
|
|
1602
1659
|
const childPaths = selectionTree.get(key) ?? emptySet;
|
|
1660
|
+
if (value === null) {
|
|
1661
|
+
result[key] = null;
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1603
1664
|
const childType = relationDescriptor.listOf;
|
|
1604
1665
|
const childConfig = this.types.get(childType);
|
|
1605
1666
|
if (!childConfig) throw new Error(`fate: Unknown related type '${childType}' (field '${type}.${key}').`);
|
|
@@ -1745,6 +1806,10 @@ var FateClient = class {
|
|
|
1745
1806
|
if (!(key in target)) target[key] = {};
|
|
1746
1807
|
const nextSelection = Object.keys(selectionWithoutArgs).length ? selectionWithoutArgs : selectionValue;
|
|
1747
1808
|
const value = record$1[key];
|
|
1809
|
+
if (value == null) {
|
|
1810
|
+
target[key] = null;
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1748
1813
|
if (Array.isArray(value)) if (nextSelection.items && typeof nextSelection.items === "object") {
|
|
1749
1814
|
const selection = nextSelection.items;
|
|
1750
1815
|
const fieldArgs = plan.args.get(fieldPath);
|
|
@@ -1912,4 +1977,16 @@ function createTRPCTransport({ byId, client, lists, mutations, queries }) {
|
|
|
1912
1977
|
}
|
|
1913
1978
|
|
|
1914
1979
|
//#endregion
|
|
1915
|
-
|
|
1980
|
+
//#region src/root.ts
|
|
1981
|
+
/**
|
|
1982
|
+
* Defines a root query for an entity type, capturing the response shape.
|
|
1983
|
+
*/
|
|
1984
|
+
function clientRoot(type) {
|
|
1985
|
+
return Object.freeze({
|
|
1986
|
+
[RootKind]: true,
|
|
1987
|
+
type
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
//#endregion
|
|
1992
|
+
export { ConnectionTag, FateClient, clientRoot, createClient, createTRPCTransport, getSelectionPlan, isViewTag, mutation, toEntityId, view };
|
package/lib/server.d.mts
CHANGED
|
@@ -164,8 +164,9 @@ type RequestMode = /** (default) Use cached data if present, otherwise fetch. */
|
|
|
164
164
|
type RequestOptions = Readonly<{
|
|
165
165
|
mode?: RequestMode;
|
|
166
166
|
}>;
|
|
167
|
-
type FateClientOptions<Mutations extends FateMutations
|
|
167
|
+
type FateClientOptions<Roots extends FateRoots, Mutations extends FateMutations> = {
|
|
168
168
|
mutations?: Mutations;
|
|
169
|
+
roots: Roots;
|
|
169
170
|
transport: Transport<MutationMapFromDefinitions<Mutations>>;
|
|
170
171
|
types: ReadonlyArray<Omit<TypeConfig, 'getId'> & Partial<{
|
|
171
172
|
getId: TypeConfig['getId'];
|
|
@@ -175,13 +176,15 @@ type FateClientOptions<Mutations extends FateMutations = EmptyMutations> = {
|
|
|
175
176
|
* Core client that normalizes records, manages the view cache, and coordinates
|
|
176
177
|
* data fetching.
|
|
177
178
|
*/
|
|
178
|
-
declare class FateClient<Mutations extends FateMutations> {
|
|
179
|
+
declare class FateClient<Roots extends FateRoots, Mutations extends FateMutations> {
|
|
179
180
|
private readonly mutationMap;
|
|
180
181
|
private readonly parentLists;
|
|
181
182
|
private readonly rootLists;
|
|
182
183
|
private readonly pending;
|
|
184
|
+
private readonly pendingOptimisticMutations;
|
|
183
185
|
private readonly optimisticMasks;
|
|
184
186
|
private readonly optimisticByEntity;
|
|
187
|
+
private readonly optimisticEntityResolutions;
|
|
185
188
|
private optimisticTokenCounter;
|
|
186
189
|
private readonly requests;
|
|
187
190
|
private readonly rootRequests;
|
|
@@ -192,7 +195,8 @@ declare class FateClient<Mutations extends FateMutations> {
|
|
|
192
195
|
private readonly viewDataCache;
|
|
193
196
|
readonly actions: MutationActionsFor<Mutations>;
|
|
194
197
|
readonly mutations: MutationFunctionsFor<Mutations>;
|
|
195
|
-
|
|
198
|
+
readonly roots: Roots;
|
|
199
|
+
constructor(options: FateClientOptions<Roots, Mutations>);
|
|
196
200
|
private initializeParentLists;
|
|
197
201
|
private registerRootList;
|
|
198
202
|
private insertIntoRootLists;
|
|
@@ -213,11 +217,14 @@ declare class FateClient<Mutations extends FateMutations> {
|
|
|
213
217
|
}>;
|
|
214
218
|
readView<T$1 extends Entity, S extends Selection<T$1>, V extends View<T$1, S>>(view: V, ref: ViewRef<T$1['__typename']>): FateThenable<ViewSnapshot<T$1, S>>;
|
|
215
219
|
private mergeListState;
|
|
220
|
+
registerPendingOptimisticMutation(entityId: EntityId, promise: Promise<unknown>): void;
|
|
221
|
+
resolveOptimisticEntity(optimisticEntityId: EntityId, resolvedEntityId: EntityId): void;
|
|
216
222
|
registerOptimisticUpdate(entityId: EntityId | null, select: ReadonlySet<string>): number | null;
|
|
217
223
|
clearOptimisticUpdate(token: number | null): void;
|
|
218
224
|
getPendingOptimisticMask(entityId: EntityId | null, options?: {
|
|
219
225
|
excludeToken?: number | null;
|
|
220
226
|
}): FieldMask | null;
|
|
227
|
+
private getPendingOptimisticMutations;
|
|
221
228
|
filterSelectionForPendingOptimistics(entityId: EntityId | null, select: Set<string>, options?: {
|
|
222
229
|
excludeToken?: number | null;
|
|
223
230
|
}): Set<string>;
|
|
@@ -228,12 +235,13 @@ declare class FateClient<Mutations extends FateMutations> {
|
|
|
228
235
|
ids: ReadonlyArray<EntityId>;
|
|
229
236
|
pagination?: Pagination;
|
|
230
237
|
}> | undefined>;
|
|
231
|
-
request<R$1 extends Request>(request: R$1, options?: RequestOptions): Promise<RequestResult<R$1>>;
|
|
238
|
+
request<R$1 extends Request>(request: R$1, options?: RequestOptions): Promise<RequestResult<Roots, R$1>>;
|
|
232
239
|
releaseRequest(request: Request, mode: RequestMode): void;
|
|
233
240
|
private handleStoreAndNetworkRequest;
|
|
241
|
+
private getRootType;
|
|
234
242
|
private executeRequest;
|
|
235
243
|
private hasRequestData;
|
|
236
|
-
getRequestResult<R$1 extends Request>(request: R$1): RequestResult<R$1>;
|
|
244
|
+
getRequestResult<R$1 extends Request>(request: R$1): RequestResult<Roots, R$1>;
|
|
237
245
|
private fetchByIdAndNormalize;
|
|
238
246
|
private fetchQueryAndNormalize;
|
|
239
247
|
private fetchListAndNormalize;
|
|
@@ -245,7 +253,7 @@ declare class FateClient<Mutations extends FateMutations> {
|
|
|
245
253
|
private pendingPrefix;
|
|
246
254
|
private pendingKey;
|
|
247
255
|
}
|
|
248
|
-
declare function createClient<
|
|
256
|
+
declare function createClient<T$1 extends [FateRoots, FateMutations] = [Record<never, RootDefinition<any, any>>, Record<never, MutationDefinition<any, any, any>>]>(options: FateClientOptions<T$1[0], T$1[1]>): FateClient<T$1[0], T$1[1]>;
|
|
249
257
|
//#endregion
|
|
250
258
|
//#region src/mutation.d.ts
|
|
251
259
|
/**
|
|
@@ -303,7 +311,6 @@ type MutationIdentifierFor<K$1 extends string, Def extends MutationDefinition<an
|
|
|
303
311
|
}> : never;
|
|
304
312
|
type UnionToIntersection<U$1> = (U$1 extends any ? (k: U$1) => void : never) extends ((k: infer I) => void) ? I : never;
|
|
305
313
|
type NestedValue<Path extends string, Value> = Path extends `${infer Head}.${infer Tail}` ? { [K in Head]: NestedValue<Tail, Value> } : { [K in Path]: Value };
|
|
306
|
-
type EmptyMutations = Record<never, MutationDefinition<any, any, any>>;
|
|
307
314
|
/**
|
|
308
315
|
* Base type for client mutations.
|
|
309
316
|
*/
|
|
@@ -328,6 +335,8 @@ declare const __FateSelectionBrand: unique symbol;
|
|
|
328
335
|
declare const __FateMutationEntityBrand: unique symbol;
|
|
329
336
|
declare const __FateMutationInputBrand: unique symbol;
|
|
330
337
|
declare const __FateMutationResultBrand: unique symbol;
|
|
338
|
+
declare const __FateRootResultBrand: unique symbol;
|
|
339
|
+
declare const __FateRootTypeBrand: unique symbol;
|
|
331
340
|
type __ViewEntityAnchor<T$1 extends Entity> = {
|
|
332
341
|
readonly [__FateEntityBrand]?: T$1;
|
|
333
342
|
};
|
|
@@ -343,12 +352,21 @@ type __MutationInputAnchor<I$1> = {
|
|
|
343
352
|
type __MutationResultAnchor<R$1> = {
|
|
344
353
|
readonly [__FateMutationResultBrand]?: R$1;
|
|
345
354
|
};
|
|
355
|
+
type __RootResultAnchor<R$1> = {
|
|
356
|
+
readonly [__FateRootResultBrand]?: R$1;
|
|
357
|
+
};
|
|
358
|
+
type __RootTypeAnchor<T$1 extends TypeName> = {
|
|
359
|
+
readonly [__FateRootTypeBrand]?: T$1;
|
|
360
|
+
};
|
|
346
361
|
/** Unique key that identifies a view composition entry inside a selection or reference. */
|
|
347
362
|
type ViewTag = `__fate-view__${string}`;
|
|
348
363
|
/** Determines whether a property key is a fate view tag. */
|
|
349
364
|
declare function isViewTag(key: string): key is ViewTag;
|
|
350
365
|
/** Alias for a loose record used throughout the fate's internals. */
|
|
351
366
|
type AnyRecord = Record<string, unknown>;
|
|
367
|
+
type Nullish<T$1> = Extract<T$1, null | undefined>;
|
|
368
|
+
type NonNullish<T$1> = Exclude<T$1, null | undefined>;
|
|
369
|
+
type WithNullish<T$1, R$1> = R$1 | Nullish<T$1>;
|
|
352
370
|
type SelectionArgs = Readonly<{
|
|
353
371
|
args: AnyRecord;
|
|
354
372
|
}>;
|
|
@@ -478,7 +496,8 @@ type EntityName<T$1> = T$1 extends {
|
|
|
478
496
|
__typename: infer N extends string;
|
|
479
497
|
} ? N : never;
|
|
480
498
|
/** Recursively applies a view selection to an entity to mask fields that aren't selected. */
|
|
481
|
-
type
|
|
499
|
+
type MaskNonNullish<T$1, S> = T$1 extends Array<infer U extends Entity> ? S extends true ? Array<U> : S extends ConnectionSelection<U> ? ConnectionMask<U, S> : HasViewTag<S> extends true ? Array<ViewRef<U['__typename']>> : Array<Mask<U, S>> : S extends true ? T$1 : S extends object ? HasViewTag<S> extends true ? NonNullish<T$1> extends Entity ? ViewRef<EntityName<NonNullish<T$1>>> : ViewRef<EntityName<NonNullable<T$1>>> : { [K in keyof S as K extends 'args' ? never : K]: S[K] extends true ? NonNullish<T$1>[Extract<K, keyof T$1>] : Mask<NonNullish<T$1>[Extract<K, keyof T$1>], Extract<S[K], object>> } & (T$1 extends Entity ? Pick<NonNullish<T$1>, '__typename'> : Record<never, never>) : T$1;
|
|
500
|
+
type Mask<T$1, S> = WithNullish<T$1, MaskNonNullish<NonNullish<T$1>, S>>;
|
|
482
501
|
/** Entity type captured from a view definition. */
|
|
483
502
|
type ViewEntity<V> = V extends View<infer T, any> ? T : never;
|
|
484
503
|
/** Name of the entity type captured from a view definition. */
|
|
@@ -491,24 +510,20 @@ type ViewSelection<V> = V extends {
|
|
|
491
510
|
type ListItem<V extends View<any, any>> = Readonly<{
|
|
492
511
|
args?: Record<string, unknown>;
|
|
493
512
|
list: V;
|
|
494
|
-
type: ViewEntityName<V>;
|
|
495
513
|
}>;
|
|
496
514
|
/** Definition of a root-level query request. */
|
|
497
515
|
type QueryItem<V extends View<any, any>> = Readonly<{
|
|
498
516
|
args?: Record<string, unknown>;
|
|
499
|
-
type: ViewEntityName<V>;
|
|
500
517
|
view: V;
|
|
501
518
|
}>;
|
|
502
519
|
/** Definition of a node request with one explicit ID for fetching data from the backend. */
|
|
503
520
|
type NodeItem<V extends View<any, any>> = Readonly<{
|
|
504
521
|
id: string | number;
|
|
505
|
-
type: ViewEntityName<V>;
|
|
506
522
|
view: V;
|
|
507
523
|
}>;
|
|
508
524
|
/** Definition of a node request with explicit IDs for fetching data from the backend. */
|
|
509
525
|
type NodesItem<V extends View<any, any>> = Readonly<{
|
|
510
526
|
ids: ReadonlyArray<string | number>;
|
|
511
|
-
type: ViewEntityName<V>;
|
|
512
527
|
view: V;
|
|
513
528
|
}>;
|
|
514
529
|
type RequestItem = ListItem<View<any, any>> | NodeItem<View<any, any>> | NodesItem<View<any, any>> | QueryItem<View<any, any>>;
|
|
@@ -530,7 +545,7 @@ type ConnectionNodeType<Root> = Root extends {
|
|
|
530
545
|
node?: infer Node;
|
|
531
546
|
};
|
|
532
547
|
} ? ViewEntityName<Node & View<any, any>> : never;
|
|
533
|
-
type ListResult<Item extends AnyRequestItem> = Item extends AnyNodeItem ? ViewRef<
|
|
548
|
+
type ListResult<Item extends AnyRequestItem, Type extends TypeName, Result> = Item extends AnyNodeItem ? ViewRef<Type> : Item extends AnyNodesItem ? Array<ViewRef<Type>> : Item extends AnyQueryItem ? Result extends null ? ViewRef<Type> | null : ViewRef<Type> : Item extends AnyListItem ? Item['list'] extends {
|
|
534
549
|
items?: {
|
|
535
550
|
node?: View<any, any>;
|
|
536
551
|
};
|
|
@@ -540,14 +555,27 @@ type ListResult<Item extends AnyRequestItem> = Item extends AnyNodeItem ? ViewRe
|
|
|
540
555
|
node: ViewRef<ConnectionNodeType<Item['list']>>;
|
|
541
556
|
}>;
|
|
542
557
|
pagination?: Pagination;
|
|
543
|
-
}> : Array<ViewRef<
|
|
558
|
+
}> : Array<ViewRef<Type>> : never;
|
|
544
559
|
/**
|
|
545
560
|
* The result of a `FateClient.request` and `useRequest` call, mapping each
|
|
546
561
|
* request key to its corresponding result.
|
|
547
562
|
*/
|
|
548
|
-
type RequestResult<Q extends AnyRequest> = { [K in keyof Q]: ListResult<Q[K]
|
|
563
|
+
type RequestResult<R$1 extends FateRoots, Q extends AnyRequest> = { [K in keyof Q]: K extends keyof R$1 ? ListResult<Q[K], RootType<R$1[K]>, RootResult<R$1[K]>> : never };
|
|
564
|
+
/** Brand used on root definitions to mark their identity in the d.ts output. */
|
|
565
|
+
declare const RootKind = "__fate__root";
|
|
549
566
|
/** Brand used on mutation definitions to mark their identity in the d.ts output. */
|
|
550
567
|
declare const MutationKind = "__fate__mutation";
|
|
568
|
+
/** Metadata describing a root query for a particular entity and result shape. */
|
|
569
|
+
type RootDefinition<Type extends TypeName, Result> = Readonly<{
|
|
570
|
+
[RootKind]: true;
|
|
571
|
+
type: Type;
|
|
572
|
+
}> & __RootResultAnchor<Result> & __RootTypeAnchor<Type>;
|
|
573
|
+
/** Minimal root description used for typing root maps. */
|
|
574
|
+
type FateRoots = Record<string, RootDefinition<TypeName, unknown>>;
|
|
575
|
+
/** Extracts the entity type name from a root definition. */
|
|
576
|
+
type RootType<R$1> = R$1 extends __RootTypeAnchor<infer T> ? T : never;
|
|
577
|
+
/** Extracts the result type from a root definition. */
|
|
578
|
+
type RootResult<R$1> = R$1 extends __RootResultAnchor<infer Data> ? Data : never;
|
|
551
579
|
/** Metadata describing a mutation for a particular entity, input, and output. */
|
|
552
580
|
type MutationDefinition<T$1 extends Entity, I$1, R$1> = Readonly<{
|
|
553
581
|
entity: T$1['__typename'];
|
|
@@ -575,8 +603,6 @@ type MutationMapFromDefinitions<D extends FateMutations> = { [K in keyof D]: {
|
|
|
575
603
|
input: MutationInput<D[K]>;
|
|
576
604
|
output: MutationResult<D[K]>;
|
|
577
605
|
} };
|
|
578
|
-
type Nullish<T$1> = Extract<T$1, null | undefined>;
|
|
579
|
-
type NonNullish<T$1> = Exclude<T$1, null | undefined>;
|
|
580
606
|
type OptimisticUpdateValue<T$1> = T$1 extends ReadonlyArray<infer U> ? Array<OptimisticUpdateValue<U>> : NonNullish<T$1> extends AnyRecord ? OptimisticUpdate<NonNullish<T$1>> | Nullish<T$1> : NonNullish<T$1> | Nullish<T$1>;
|
|
581
607
|
/** Shape used to describe optimistic updates for mutations. */
|
|
582
608
|
type OptimisticUpdate<T$1> = { [K in keyof T$1]?: OptimisticUpdateValue<T$1[K]> };
|
|
@@ -591,4 +617,4 @@ interface FateThenable<T$1> extends PromiseLike<T$1> {
|
|
|
591
617
|
value: T$1;
|
|
592
618
|
}
|
|
593
619
|
//#endregion
|
|
594
|
-
export {
|
|
620
|
+
export { ViewSelection as A, RequestOptions as B, TypeConfig as C, ViewEntity as D, ViewData as E, isViewTag as F, Transport as H, FateMutations as I, mutation as L, ViewTag as M, __FateEntityBrand as N, ViewEntityName as O, __FateSelectionBrand as P, FateClient as R, Snapshot as S, View as T, createTRPCTransport as U, createClient as V, getSelectionPlan as W, Pagination as _, Entity as a, RootDefinition as b, FateThenable as c, MutationDefinition as d, MutationEntity as f, NodesItem as g, MutationResult as h, ConnectionTag as i, ViewSnapshot as j, ViewRef as k, ListItem as l, MutationInput as m, ConnectionMetadata as n, EntityId as o, MutationIdentifier as p, ConnectionRef as r, FateRoots as s, AnyRecord as t, Mask as u, Request as v, TypeName as w, Selection as x, RequestResult as y, RequestMode as z };
|