@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 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, allowing AI tools to generate stable, type-safe data-fetching code.
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
- 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:
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 `HomePage` component, we can request a list of posts like this:
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 HomePage() {
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
- 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:
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 { useRequest, useView, ViewRef } from 'react-fate';
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
- ### Request Modes
432
+ ## Requests
447
433
 
448
- `useRequest` supports different request modes to control caching and data freshness. The available modes are:
434
+ ### Requesting Lists
449
435
 
450
- - `cache-first` (_default_): Returns data from the cache if available, otherwise fetches from the network.
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
- You can pass the request mode as an option to `useRequest`:
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
- const { posts } = useRequest(
458
- {
459
- posts: { root: PostView, type: 'Post' },
460
- },
461
- { mode: 'stale-while-revalidate' },
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
- root: PostView,
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
- } as const;
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
- } as const;
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 the instance of the fate client, and using it in our React app using the `FateClient` context provider.
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 { fate } from './fate.ts';
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. It lacks core features such as garbage collection, a compiler to extract view definitions statically and 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:
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 __FateEntityBrand, B as createTRPCTransport, C as ViewData, D as ViewSelection, E as ViewRef, F as FateClient, I as RequestMode, L as RequestOptions, M as isViewTag, N as FateMutations, O as ViewSnapshot, P as mutation, R as createClient, S as View, T as ViewEntityName, V as getSelectionPlan, _ as RequestResult, a as Entity, b as TypeConfig, c as Mask, d as MutationIdentifier, f as MutationInput, g as Request, h as Pagination, i as ConnectionTag, j as __FateSelectionBrand, k as ViewTag, l as MutationDefinition, m as NodesItem, n as ConnectionMetadata, o as EntityId, p as MutationResult, r as ConnectionRef, s as ListItem, t as AnyRecord, u as MutationEntity, v as Selection, w as ViewEntity, x as TypeName, y as Snapshot, z as Transport } from "./types-BPmcnouE.mjs";
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/ref.d.ts
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
- try {
691
- const result = await client.executeMutation(identifier.key, input, selection, {
692
- args,
693
- plan
694
- });
695
- if (result && typeof result === "object" && (!deleteRecord || Boolean(view$1))) {
696
- const select = collectImplicitSelectedPaths(result);
697
- const pendingMask = optimisticEntityId ? client.getPendingOptimisticMask(optimisticEntityId, { excludeToken: optimisticToken }) : null;
698
- const filteredSelection = optimisticEntityId ? client.filterSelectionForPendingOptimistics(optimisticEntityId, select, { excludeToken: optimisticToken }) : select;
699
- client.write(identifier.entity, result, filteredSelection, void 0, plan, null, pendingMask, insert);
700
- if (deleteRecord && id$1 != null) client.deleteRecord(identifier.entity, id$1);
701
- const resultId = maybeGetId(config.getId, result);
702
- if (optimisticRecordId != null && resultId != null && optimisticRecordId !== resultId) client.deleteRecord(identifier.entity, optimisticRecordId);
703
- }
704
- return {
705
- error: void 0,
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: void 0
709
+ error: void 0,
710
+ result
718
711
  };
719
- } else throw new Error(`fate: Mutation '${identifier.key}' failed.`);
720
- } finally {
721
- client.clearOptimisticUpdate(optimisticToken);
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}:${item.type}:${getViewSignature(item.view)}:${item.id}`);
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}:${item.type}:${getViewSignature(item.view)}:${item.ids.map(serializeId).join(",")}`);
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}:${item.type}:${getViewSignature(item.view)}:${item.args ? hashArgs(item.args) : ""}`);
1025
+ parts.push(`query:${name}:${getViewSignature(item.view)}:${item.args ? hashArgs(item.args) : ""}`);
1018
1026
  continue;
1019
1027
  }
1020
- parts.push(`list:${name}:${item.type}:${getViewSignature(item.list)}:${item.args ? hashArgs(item.args) : ""}`);
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 fieldsSignature = [...fields].slice().sort().join(",");
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: item.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(item.type, raw);
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(item.type, raw);
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(item.type, item.id, item.view);
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(item.type, id$1, item.view));
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(item.type, id$1, item.view, { root: true });
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: item.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(item.type, record, plan.paths, void 0, plan);
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(item.type, entry.node, plan.paths, void 0, plan);
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(item.type, name);
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
- export { ConnectionTag, FateClient, createClient, createTRPCTransport, getSelectionPlan, isViewTag, mutation, toEntityId, view };
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
@@ -1,4 +1,4 @@
1
- import { t as AnyRecord } from "./types-BPmcnouE.mjs";
1
+ import { t as AnyRecord } from "./types-BiDLhb07.mjs";
2
2
  import { z } from "zod";
3
3
  import { TRPCProcedureBuilder } from "@trpc/server";
4
4
 
@@ -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 = EmptyMutations> = {
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
- constructor(options: FateClientOptions<Mutations>);
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<Mutations extends FateMutations = EmptyMutations>(options: FateClientOptions<Mutations>): FateClient<Mutations>;
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 Mask<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 ? ViewRef<EntityName<NonNullable<T$1>>> : { [K in keyof S as K extends 'args' ? never : K]: S[K] extends true ? NonNullable<T$1>[Extract<K, keyof T$1>] : Mask<NonNullable<T$1>[Extract<K, keyof T$1>], Extract<S[K], object>> } & (T$1 extends Entity ? Pick<NonNullable<T$1>, '__typename'> : Record<never, never>) : T$1;
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<Item['type']> : Item extends AnyNodesItem ? Array<ViewRef<Item['type']>> : Item extends AnyQueryItem ? ViewRef<Item['type']> : Item extends AnyListItem ? Item['list'] extends {
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<Item['type']>> : never;
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 { __FateEntityBrand as A, createTRPCTransport as B, ViewData as C, ViewSelection as D, ViewRef as E, FateClient as F, RequestMode as I, RequestOptions as L, isViewTag as M, FateMutations as N, ViewSnapshot as O, mutation as P, createClient as R, View as S, ViewEntityName as T, getSelectionPlan as V, RequestResult as _, Entity as a, TypeConfig as b, Mask as c, MutationIdentifier as d, MutationInput as f, Request as g, Pagination as h, ConnectionTag as i, __FateSelectionBrand as j, ViewTag as k, MutationDefinition as l, NodesItem as m, ConnectionMetadata as n, EntityId as o, MutationResult as p, ConnectionRef as r, ListItem as s, AnyRecord as t, MutationEntity as u, Selection as v, ViewEntity as w, TypeName as x, Snapshot as y, Transport as z };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nkzw/fate",
3
- "version": "0.0.8",
3
+ "version": "0.1.1",
4
4
  "description": "fate is a modern data client for React.",
5
5
  "homepage": "https://github.com/nkzw-tech/fate",
6
6
  "repository": {