@nkzw/fate 1.3.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +326 -0
- package/docs/api/classes/FateClient.md +51 -199
- package/docs/api/functions/clientRoot.md +1 -1
- package/docs/api/functions/createClient.md +1 -1
- package/docs/api/functions/createGraphQLTransport.md +1 -1
- package/docs/api/functions/createHTTPTransport.md +1 -1
- package/docs/api/functions/createTRPCTransport.md +5 -1
- package/docs/api/functions/defer.md +1 -1
- package/docs/api/functions/getDeferredMetadata.md +1 -1
- package/docs/api/functions/getListEntries.md +1 -1
- package/docs/api/functions/getSelectionPlan.md +1 -1
- package/docs/api/functions/graphqlMutation.md +1 -1
- package/docs/api/functions/isDeferred.md +1 -1
- package/docs/api/functions/isRecord.md +1 -1
- package/docs/api/functions/isViewTag.md +1 -1
- package/docs/api/functions/liveConnectionTopic.md +1 -1
- package/docs/api/functions/liveEntityTopic.md +1 -1
- package/docs/api/functions/liveGlobalConnectionTopic.md +1 -1
- package/docs/api/functions/mutation.md +1 -1
- package/docs/api/functions/toEntityId.md +1 -1
- package/docs/api/functions/view.md +1 -1
- package/docs/api/index.md +4 -1
- package/docs/api/interfaces/FateThenable.md +3 -3
- package/docs/api/interfaces/Persistence.md +23 -0
- package/docs/api/interfaces/PersistenceSession.md +109 -0
- package/docs/api/interfaces/Transport.md +46 -7
- package/docs/api/server/classes/FateRequestError.md +5 -5
- package/docs/api/server/drizzle/functions/createDrizzleFate.md +1 -1
- package/docs/api/server/drizzle/functions/createDrizzleSourceAdapter.md +1 -1
- package/docs/api/server/drizzle/functions/createDrizzleSourceRegistry.md +1 -1
- package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyConfig.md +1 -1
- package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyInput.md +1 -1
- package/docs/api/server/drizzle/type-aliases/DrizzleQueryExtra.md +3 -3
- package/docs/api/server/drizzle/type-aliases/DrizzleSourceAdapter.md +9 -9
- package/docs/api/server/drizzle/type-aliases/DrizzleViewConfig.md +1 -1
- package/docs/api/server/functions/bindSourceProcedures.md +1 -1
- package/docs/api/server/functions/computed.md +1 -1
- package/docs/api/server/functions/count.md +1 -1
- package/docs/api/server/functions/createFateFetchHandler.md +1 -1
- package/docs/api/server/functions/createFateServer.md +1 -1
- package/docs/api/server/functions/createHonoFateHandler.md +1 -1
- package/docs/api/server/functions/createLiveEventBus.md +1 -1
- package/docs/api/server/functions/createNestedSourcePlan.md +1 -1
- package/docs/api/server/functions/createResolver.md +1 -1
- package/docs/api/server/functions/createSourcePlan.md +1 -1
- package/docs/api/server/functions/createSourceProcedures.md +1 -1
- package/docs/api/server/functions/dataView.md +1 -1
- package/docs/api/server/functions/field.md +1 -1
- package/docs/api/server/functions/getNestedSelection.md +1 -1
- package/docs/api/server/functions/getScopedArgs.md +1 -1
- package/docs/api/server/functions/hasNestedSelection.md +1 -1
- package/docs/api/server/functions/list.md +1 -1
- package/docs/api/server/functions/refetchSourceById.md +1 -1
- package/docs/api/server/functions/resolveSourceById.md +1 -1
- package/docs/api/server/functions/resolveSourceByIds.md +1 -1
- package/docs/api/server/functions/resolveSourceConnection.md +1 -1
- package/docs/api/server/functions/resolver.md +1 -1
- package/docs/api/server/functions/toPrismaSelect.md +1 -1
- package/docs/api/server/functions/withConnection.md +1 -1
- package/docs/api/server/prisma/functions/createPrismaFate.md +1 -1
- package/docs/api/server/prisma/functions/createPrismaSourceAdapter.md +1 -1
- package/docs/api/server/prisma/functions/createPrismaSourceRegistry.md +1 -1
- package/docs/api/server/prisma/functions/prismaConnectionArgs.md +1 -1
- package/docs/api/server/prisma/type-aliases/PrismaDelegate.md +4 -4
- package/docs/api/server/prisma/type-aliases/PrismaQueryExtra.md +1 -1
- package/docs/api/server/prisma/type-aliases/PrismaSourceAdapter.md +9 -9
- package/docs/api/server/prisma/type-aliases/PrismaViewConfig.md +1 -1
- package/docs/api/server/type-aliases/ComputedField.md +5 -5
- package/docs/api/server/type-aliases/ComputedSelection.md +1 -1
- package/docs/api/server/type-aliases/ConnectionItem.md +3 -3
- package/docs/api/server/type-aliases/ConnectionPagination.md +5 -5
- package/docs/api/server/type-aliases/ConnectionResult.md +3 -3
- package/docs/api/server/type-aliases/CountSelection.md +4 -4
- package/docs/api/server/type-aliases/CountWhere.md +1 -1
- package/docs/api/server/type-aliases/DataViewListOptions.md +2 -2
- package/docs/api/server/type-aliases/DataViewOrderBy.md +1 -1
- package/docs/api/server/type-aliases/DataViewOrderDirection.md +1 -1
- package/docs/api/server/type-aliases/DataViewResult.md +1 -1
- package/docs/api/server/type-aliases/Entity.md +1 -1
- package/docs/api/server/type-aliases/FateServer.md +5 -5
- package/docs/api/server/type-aliases/FateServerManifest.md +1 -1
- package/docs/api/server/type-aliases/FieldSelection.md +3 -3
- package/docs/api/server/type-aliases/LiveConnectionEventType.md +1 -1
- package/docs/api/server/type-aliases/LiveConnectionSourceEvent.md +1 -1
- package/docs/api/server/type-aliases/LiveEventBus.md +1 -1
- package/docs/api/server/type-aliases/LiveEventType.md +1 -1
- package/docs/api/server/type-aliases/LiveSourceEvent.md +1 -1
- package/docs/api/server/type-aliases/NativeFateAPI.md +4 -4
- package/docs/api/server/type-aliases/OrderDirection.md +1 -1
- package/docs/api/server/type-aliases/SourceConfig.md +6 -6
- package/docs/api/server/type-aliases/SourceDefinition.md +6 -6
- package/docs/api/server/type-aliases/SourceOrder.md +1 -1
- package/docs/api/server/type-aliases/SourceOrderField.md +3 -3
- package/docs/api/server/type-aliases/SourcePlan.md +1 -1
- package/docs/api/server/type-aliases/SourcePlanNode.md +1 -1
- package/docs/api/server/type-aliases/SourceRegistry.md +1 -1
- package/docs/api/server/type-aliases/SourceRelation.md +1 -1
- package/docs/api/server/type-aliases/SourceRelationConfig.md +1 -1
- package/docs/api/server/variables/byIdInput.md +1 -1
- package/docs/api/server/variables/connectionArgs.md +1 -1
- package/docs/api/type-aliases/ConnectionMetadata.md +1 -1
- package/docs/api/type-aliases/ConnectionRef.md +1 -1
- package/docs/api/type-aliases/Deferred.md +1 -1
- package/docs/api/type-aliases/DeferredMetadata.md +1 -1
- package/docs/api/type-aliases/DeferredSelection.md +1 -1
- package/docs/api/type-aliases/DeferredSnapshot.md +1 -1
- package/docs/api/type-aliases/Entity.md +2 -2
- package/docs/api/type-aliases/EntityId.md +1 -1
- package/docs/api/type-aliases/FateDehydratedState.md +1 -1
- package/docs/api/type-aliases/FateLiveConnectionEvent.md +1 -1
- package/docs/api/type-aliases/FateLiveEvent.md +1 -1
- package/docs/api/type-aliases/FateMutations.md +1 -1
- package/docs/api/type-aliases/FateOperation.md +2 -2
- package/docs/api/type-aliases/FateProtocolRequest.md +1 -1
- package/docs/api/type-aliases/FateProtocolResponse.md +1 -1
- package/docs/api/type-aliases/FateRecord.md +1 -1
- package/docs/api/type-aliases/FateRoots.md +1 -1
- package/docs/api/type-aliases/GraphQLMutationDefinition.md +1 -1
- package/docs/api/type-aliases/GraphQLMutationInput.md +1 -1
- package/docs/api/type-aliases/GraphQLMutationMap.md +1 -1
- package/docs/api/type-aliases/GraphQLMutationOutput.md +1 -1
- package/docs/api/type-aliases/GraphQLTransportOptions.md +19 -11
- package/docs/api/type-aliases/HydrateOptions.md +1 -1
- package/docs/api/type-aliases/HydrationLimits.md +1 -1
- package/docs/api/type-aliases/InferFateAPI.md +1 -1
- package/docs/api/type-aliases/List.md +1 -1
- package/docs/api/type-aliases/ListEntry.md +1 -1
- package/docs/api/type-aliases/ListItem.md +1 -1
- package/docs/api/type-aliases/Mask.md +1 -1
- package/docs/api/type-aliases/MutationDefinition.md +1 -1
- package/docs/api/type-aliases/MutationEntity.md +1 -1
- package/docs/api/type-aliases/MutationIdentifier.md +1 -1
- package/docs/api/type-aliases/MutationIdentity.md +7 -0
- package/docs/api/type-aliases/MutationInput.md +1 -1
- package/docs/api/type-aliases/MutationResult.md +1 -1
- package/docs/api/type-aliases/NodesItem.md +1 -1
- package/docs/api/type-aliases/Pagination.md +5 -5
- package/docs/api/type-aliases/PersistenceSnapshot.md +5 -0
- package/docs/api/type-aliases/Request.md +1 -1
- package/docs/api/type-aliases/RequestMode.md +1 -1
- package/docs/api/type-aliases/RequestOptions.md +2 -2
- package/docs/api/type-aliases/RequestResult.md +1 -1
- package/docs/api/type-aliases/Selection.md +1 -1
- package/docs/api/type-aliases/TypeConfig.md +4 -4
- package/docs/api/type-aliases/View.md +1 -1
- package/docs/api/type-aliases/ViewData.md +1 -1
- package/docs/api/type-aliases/ViewEntity.md +1 -1
- package/docs/api/type-aliases/ViewEntityName.md +1 -1
- package/docs/api/type-aliases/ViewRef.md +1 -1
- package/docs/api/type-aliases/ViewSelection.md +1 -1
- package/docs/api/type-aliases/ViewSnapshot.md +1 -1
- package/docs/api/type-aliases/ViewTag.md +1 -1
- package/docs/api/variables/ConnectionTag.md +1 -1
- package/docs/api/variables/DeferTag.md +1 -1
- package/docs/api/variables/DeferredTag.md +1 -1
- package/docs/guide/getting-started.md +2 -0
- package/docs/guide/persistence.md +323 -0
- package/lib/{liveTopics-uVP0LG-Q.mjs → args-CRLIRl_v.mjs} +1 -59
- package/lib/cli.d.mts +1 -1
- package/lib/{executor-C8HNogYG.d.mts → executor-DoiTBiVv.d.mts} +18 -65
- package/lib/{graphqlTransport-DskuXeUP.d.mts → graphqlTransport-DGLYyRxL.d.mts} +4 -14
- package/lib/{graphqlTransport-DSBVS2Fq.mjs → graphqlTransport-DmheG3AO.mjs} +3 -2
- package/lib/graphqlTransport.d.mts +1 -1
- package/lib/graphqlTransport.mjs +1 -1
- package/lib/hydration-DPZRJgnu.mjs +273 -0
- package/lib/idempotency-types-CqA-BFpn.d.mts +15 -0
- package/lib/idempotency.d.mts +26 -0
- package/lib/idempotency.mjs +34 -0
- package/lib/index.d.mts +6 -15
- package/lib/index.mjs +560 -1774
- package/lib/{list-sI2UPhHc.d.mts → list-BRT1VtiT.d.mts} +1 -2
- package/lib/list.d.mts +1 -1
- package/lib/liveTopics-DuFkDZQm.mjs +17 -0
- package/lib/persistence.d.mts +38 -0
- package/lib/persistence.mjs +1529 -0
- package/lib/protocol-DS5sf6sd.mjs +44 -0
- package/lib/{record-CZyyeLa3.d.mts → record-DITM4zb_.d.mts} +14 -19
- package/lib/server/drizzle.d.mts +45 -114
- package/lib/server/drizzle.mjs +2 -2
- package/lib/server/prisma.d.mts +52 -127
- package/lib/server/prisma.mjs +9 -8
- package/lib/server.d.mts +4 -16
- package/lib/server.mjs +21 -35
- package/lib/sortObjectKeys-yhuitEQZ.mjs +9 -0
- package/lib/{sourceRouter-DSaADXSf.mjs → sourceRouter-51POBlEE.mjs} +9 -6
- package/lib/store-CeatPJeK.mjs +1350 -0
- package/lib/{transport-DhaVVq4P.d.mts → transport-DqjhjLfC.d.mts} +672 -517
- package/lib/vite.d.mts +0 -1
- package/lib/vite.mjs +19 -11
- package/package.json +15 -5
- package/docs/api/type-aliases/Snapshot.md +0 -7
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# Persistence
|
|
2
|
+
|
|
3
|
+
fate can optionally keep data available across page reloads and save mutations while a user is offline. When the user comes back, their views can show previously loaded data and their pending changes continue where they left off.
|
|
4
|
+
|
|
5
|
+
Persistence builds on fate's normalized cache, [Requests](requests.md), and [Actions](actions.md). You configure storage only once, without changing how you use fate.
|
|
6
|
+
|
|
7
|
+
## Client Setup
|
|
8
|
+
|
|
9
|
+
The persistence layer is part of `@nkzw/fate`, with storage adapters installed separately. For a browser app using IndexedDB, add the adapter:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
vp add @nkzw/fate-indexeddb
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then pass `persistence` when creating the client:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { createPersistence } from '@nkzw/fate/persistence';
|
|
19
|
+
import { indexedDB } from '@nkzw/fate-indexeddb';
|
|
20
|
+
import { createFateClient } from 'react-fate/client';
|
|
21
|
+
|
|
22
|
+
const fate = createFateClient({
|
|
23
|
+
persistence: createPersistence({
|
|
24
|
+
key: `workspace:${workspaceId}:user:${userId}`,
|
|
25
|
+
storage: indexedDB(),
|
|
26
|
+
}),
|
|
27
|
+
url: '/api/fate',
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
By default, fate keeps loaded data for **one day**, with a storage budget of **25 MiB**. You can change both values:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
persistence: createPersistence({
|
|
35
|
+
key: `workspace:${workspaceId}:user:${userId}`,
|
|
36
|
+
maxAge: 24 * 60 * 60 * 1000,
|
|
37
|
+
maxBytes: 25 * 1024 * 1024,
|
|
38
|
+
storage: indexedDB(),
|
|
39
|
+
}),
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Create the browser client after you know which user is signed in. The `key` separates saved data and mutations by account and workspace, and must match the authenticated scope in the [server setup](#server-deduplication). When switching accounts, dispose the previous persistence session and create a new client with the new account's key.
|
|
43
|
+
|
|
44
|
+
> [!NOTE]
|
|
45
|
+
>
|
|
46
|
+
> Persistence saves your app's data. To load the app itself while offline, you'll also need a service worker that makes its HTML, JavaScript, and other assets available. fate does not install a service worker or download pages the user hasn't visited.
|
|
47
|
+
|
|
48
|
+
## Cache Lifetime
|
|
49
|
+
|
|
50
|
+
You can keep the data for a specific screen longer by passing `persist` to `useRequest`. For example, to keep a list of posts for three days:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
const { posts } = useRequest(
|
|
54
|
+
{
|
|
55
|
+
posts: { list: PostView },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
persist: { maxAge: 3 * 24 * 60 * 60 * 1000 },
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The same option works with `fate.request(...)` and Vue's `useRequest`. If a screen uses several requests, set the option on each request whose data you want to keep longer.
|
|
64
|
+
|
|
65
|
+
fate stores objects by their type and ID, just like the in-memory cache. If a post appears in both your feed and a detail screen, both requests share the same saved post. Each request describes which fields and related objects it needs, including list membership and pagination state.
|
|
66
|
+
|
|
67
|
+
For example, your feed might keep posts for one day, while the detail screen keeps them for three days. After one day, the fields needed by the detail screen remain available. Fields selected only by the feed can be removed. You don't need to coordinate separate copies of the same post or manually patch each request's cache.
|
|
68
|
+
|
|
69
|
+
`maxAge` is measured in milliseconds from when the data was fetched successfully. Reading saved data or rendering the screen again does not restart that lifetime. If a request fetches one missing post, it only renews the data it fetched. Other posts already in the cache keep their original age. Changing a cached request's `maxAge` also uses the original fetch time.
|
|
70
|
+
|
|
71
|
+
When callers share a pending request, the latest explicit `persist.maxAge` also applies to that request's eventual cache write.
|
|
72
|
+
|
|
73
|
+
Pass `persist: { maxAge: 0 }` to skip saving data for a request. Shared objects may still be saved for other requests, so this option does not delete all copies of an object.
|
|
74
|
+
|
|
75
|
+
The [in-memory garbage collector](requests.md#cache-lifetime) continues to work independently. A post can be removed from memory while its saved copy remains available for a later visit. On startup, fate restores pending mutations, then loads saved data as requests need it. It does not load the entire saved cache into memory.
|
|
76
|
+
|
|
77
|
+
## Refreshing Data
|
|
78
|
+
|
|
79
|
+
Keeping data for three days doesn't mean waiting three days for updates. You can use the existing [request modes](requests.md#request-modes) to choose when to fetch fresh data:
|
|
80
|
+
|
|
81
|
+
- `cache-first` (_default_): Uses available saved data and fetches missing data from the network.
|
|
82
|
+
- `stale-while-revalidate`: Shows saved data and refreshes it in the background. If the refresh fails, the saved data stays visible.
|
|
83
|
+
- `network-only`: Requires a network response, even if saved data is available.
|
|
84
|
+
|
|
85
|
+
For example, to show the previous session's posts immediately and update them on reload:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const { posts } = useRequest(
|
|
89
|
+
{
|
|
90
|
+
posts: { list: PostView },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
mode: 'stale-while-revalidate',
|
|
94
|
+
persist: { maxAge: 3 * 24 * 60 * 60 * 1000 },
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Expiration controls how long data stays in storage. It does not remove data from a mounted view or change the in-memory fetch policy. For ongoing server updates, use [Live Views](live-views.md).
|
|
100
|
+
|
|
101
|
+
## Cache Size
|
|
102
|
+
|
|
103
|
+
When saved data reaches `maxBytes`, fate removes expired data first, then releases the least recently used requests until the new data fits. Objects still needed by another saved request remain available, and shared objects count toward the budget only once. A response that is too large to save can still be used in memory without removing other saved requests to make room for it.
|
|
104
|
+
|
|
105
|
+
The budget includes encoded data, keys, cache metadata, and the saved mutation queue. Your storage backend may use additional space for its own bookkeeping. `maxBytes` controls saved data; the existing garbage collector controls the lifetime of data in memory.
|
|
106
|
+
|
|
107
|
+
fate batches cache writes and yields during large traversals and writes so other work on the page can continue. The mutation journal stores each entry separately and writes only changed entries, so confirming a mutation does not rewrite the entire queue. Changes to scalar fields write the affected record, while changes to relationships also update the saved data that depends on them.
|
|
108
|
+
|
|
109
|
+
Pending mutations are never removed to make room for cached data. If a new mutation cannot fit, it fails before fate applies its optimistic update or sends it to the server.
|
|
110
|
+
|
|
111
|
+
> [!NOTE]
|
|
112
|
+
>
|
|
113
|
+
> Already accepted mutations must still be able to finish. Their recovery data and saved results can exceed the budget if they grow or you lower `maxBytes`. In that case, fate releases the read cache and rejects new durable mutations until capacity is available. Local confirmation receipts count toward the budget until their cache changes have been saved. fate then removes them automatically, making room for new mutations.
|
|
114
|
+
|
|
115
|
+
## Actions & Mutations
|
|
116
|
+
|
|
117
|
+
With persistence configured, fate saves actions and mutations locally before applying their optimistic updates or sending them to the server. The API is the same as for regular [Actions](actions.md):
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
const [result, like] = useActionState(fate.actions.post.like, null);
|
|
121
|
+
|
|
122
|
+
like({
|
|
123
|
+
input: { id: post.id },
|
|
124
|
+
optimistic: { likes: post.likes + 1 },
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
If the user likes a post while offline, the like count updates immediately. Reloading the page restores the pending action and its optimistic update. When the connection returns and a client is running, fate sends the action to the server and updates the post with the confirmed result.
|
|
129
|
+
|
|
130
|
+
Mutations from one client are saved in invocation order and sent in the order they were saved, one at a time for each persistence key. Tabs sharing that key coordinate delivery. Temporary network and server failures are retried with increasing delays, up to 30 seconds between attempts. Authentication failures stay pending so delivery can resume after the user signs in again. A terminal client error rolls back the optimistic update and follows fate's existing [error handling](actions.md#action--mutation-error-handling).
|
|
131
|
+
|
|
132
|
+
The mutation promise resolves after remote confirmation has been saved locally. It can remain pending while offline. Closing the page loses the JavaScript promise, but the saved mutation and optimistic update remain. After a reload, use the [persistence state](#persistence-state) to show pending and failed changes.
|
|
133
|
+
|
|
134
|
+
### Skipping Persistence
|
|
135
|
+
|
|
136
|
+
For a call that should run immediately without being saved or retried, pass `persist: false`:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
await fate.mutations.analytics.record({
|
|
140
|
+
input: { event: 'opened-settings' },
|
|
141
|
+
persist: false,
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The same option works with `fate.actions`. A successful result can still update the normal cache. Without persistence configured, actions and mutations keep their existing behavior.
|
|
146
|
+
|
|
147
|
+
### Creating Objects Offline
|
|
148
|
+
|
|
149
|
+
Use a stable, client-generated ID when creating an object offline. For example, a new comment and a later edit to that comment can use the same ID, allowing fate to send the creation before the edit when the user reconnects.
|
|
150
|
+
|
|
151
|
+
If your server assigns IDs, wait for the creation result before constructing a dependent mutation. fate does not rewrite arbitrary foreign keys inside saved inputs. A failed creation also does not automatically cancel later mutations; your server should validate them as usual.
|
|
152
|
+
|
|
153
|
+
Mutation inputs and optimistic updates must be serializable with fate's hydration codec. Functions, streams, and `File` objects cannot be queued. For uploads, save the content first and queue a reference to it, or use `persist: false` for the upload itself.
|
|
154
|
+
|
|
155
|
+
## Server Deduplication
|
|
156
|
+
|
|
157
|
+
A connection can fail after the server has already applied a mutation. For example, the server might increment a post's like count, but the response never reaches the browser. Retrying that mutation without server support would increment the count twice.
|
|
158
|
+
|
|
159
|
+
fate assigns an identity to each saved mutation and reuses it on every attempt. The server records the result alongside the mutation's database changes, in **the same transaction**. When the same mutation arrives again, the server returns the saved result.
|
|
160
|
+
|
|
161
|
+
For the native HTTP transport, configure `createMutationIdempotency` on your server. The following example uses application-provided helpers to lock a mutation identity and read or insert its receipt:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { createMutationIdempotency } from '@nkzw/fate/persistence/server';
|
|
165
|
+
import { createFateServer } from '@nkzw/fate/server';
|
|
166
|
+
|
|
167
|
+
const server = createFateServer({
|
|
168
|
+
// ...roots, sources, mutations, context...
|
|
169
|
+
idempotency: createMutationIdempotency({
|
|
170
|
+
scope: (ctx) => `workspace:${ctx.workspace.id}:user:${ctx.user.id}`,
|
|
171
|
+
store: {
|
|
172
|
+
transaction: (ctx, scope, id, run) =>
|
|
173
|
+
database.transaction(async (tx) => {
|
|
174
|
+
await lockMutationIdentity(tx, scope, id);
|
|
175
|
+
return run({
|
|
176
|
+
context: { ...ctx, db: tx },
|
|
177
|
+
read: () => readReceipt(tx, scope, id),
|
|
178
|
+
write: (receipt) => insertReceipt(tx, scope, id, receipt),
|
|
179
|
+
});
|
|
180
|
+
}),
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Your app supplies `database`, `lockMutationIdentity`, `readReceipt`, and `insertReceipt`. The lock must serialize attempts with the same `(scope, id)` across server processes, including the first attempt before a receipt exists. Mutation resolvers must use the transaction's `ctx.db`, so their changes and the receipt commit together. A unique index on the receipt table alone cannot protect changes made outside that transaction.
|
|
187
|
+
|
|
188
|
+
See [`example/persistence/server.ts`](https://github.com/nkzw-tech/fate/blob/main/example/persistence/server.ts) for a complete SQLite implementation, including the receipt table and transaction handling.
|
|
189
|
+
|
|
190
|
+
The helper checks the identity against the authenticated scope and rejects reuse with different mutation names, inputs, or selections. Server receipts do not expire: a user might reconnect much later with a mutation whose response was lost. Keep receipts for as long as an old mutation could still arrive.
|
|
191
|
+
|
|
192
|
+
A database transaction cannot roll back an email, payment, or webhook sent to another service. For those effects, use a transactional outbox and the destination's idempotency support. The transaction integration guarantees one committed database effect; network requests and resolver attempts can still happen more than once.
|
|
193
|
+
|
|
194
|
+
Native HTTP sends durable mutations and receipt-only recovery using protocol version 2, so older servers reject unsupported requests before executing them. Ordinary requests and live subscriptions continue to use version 1. A current server without the idempotency integration also rejects durable mutations before running their resolvers.
|
|
195
|
+
|
|
196
|
+
## tRPC, GraphQL, and Custom Transports
|
|
197
|
+
|
|
198
|
+
For tRPC, GraphQL, or a custom transport, provide a `mutateDurably(name, input, select, identity)` method that sends the identity to an endpoint with server deduplication. Regular calls continue to use `mutate(name, input, select)`.
|
|
199
|
+
|
|
200
|
+
Both `createTRPCTransport` and `createGraphQLTransport`, including their generated clients, accept `mutateDurably`. For example, you can use a native fate endpoint for durable mutations alongside your existing transport:
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
const durableHTTP = createHTTPTransport<MyAPI>({
|
|
204
|
+
url: '/api/fate',
|
|
205
|
+
// Use the same authenticated headers as your other transport.
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const fate = createFateClient({
|
|
209
|
+
// ...your generated tRPC or GraphQL client options...
|
|
210
|
+
persistence: createPersistence({ key: accountKey, storage: indexedDB() }),
|
|
211
|
+
mutateDurably: durableHTTP.mutateDurably,
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Both endpoints need to agree on mutation names, inputs, results, and entity IDs. A custom GraphQL adapter should unwrap its response and decode GraphQL global IDs before returning data to fate.
|
|
216
|
+
|
|
217
|
+
You can also use the same idempotency helper inside an existing tRPC or GraphQL resolver. Validate the input and identity at the endpoint, then pass them to `execute`:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
return idempotency.execute({
|
|
221
|
+
ctx,
|
|
222
|
+
identity: input.identity,
|
|
223
|
+
input: input.update,
|
|
224
|
+
name: 'post.update',
|
|
225
|
+
select: input.select,
|
|
226
|
+
resolve: (transactionContext) => updatePost(transactionContext, input.update),
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The adapter must pass the entire identity on every attempt, including `replayOnly` when present. With `replayOnly: true`, the endpoint must return the existing receipt or a 404 if none exists, without executing the mutation. `createMutationIdempotency` handles both delivery and receipt-only recovery. Adding an identity to a header that the server ignores does not prevent duplicate effects.
|
|
231
|
+
|
|
232
|
+
If a client has registered mutations but no durable adapter, the persistence session reports a configuration error. Saved reads and `persist: false` calls still work. New durable calls fail before being queued, and existing queued mutations stay saved until a client with the adapter can deliver them.
|
|
233
|
+
|
|
234
|
+
## Persistence State
|
|
235
|
+
|
|
236
|
+
Use `fate.persistence` to observe pending changes and storage errors:
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
const session = fate.persistence!;
|
|
240
|
+
await session.ready;
|
|
241
|
+
|
|
242
|
+
const unsubscribe = session.subscribe(() => {
|
|
243
|
+
const { status, error, mutations } = session.getSnapshot();
|
|
244
|
+
// Show pending changes or report a storage error.
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`getSnapshot` returns a stable value between updates and can be used with React's `useSyncExternalStore`. The same subscription API works with Vue. Each mutation exposes its ID, name, input, status (`queued`, `sending`, or `failed`), and last error.
|
|
249
|
+
|
|
250
|
+
A failed initial restoration rejects `ready`. Storage errors also appear in the snapshot. If reading the saved cache fails, fate can still fetch the data from the network. If saving a new mutation fails, the call rejects before it is sent.
|
|
251
|
+
|
|
252
|
+
Cache writes are batched. To save the latest read cache before a reload you control, call `flush()`:
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
await fate.persistence!.flush();
|
|
256
|
+
window.location.reload();
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
A failed cache write keeps its pending data for a later `flush()` retry. Mutations are saved individually before delivery and do not depend on a delayed cache write or a page-unload event. On confirmation, fate saves the receipt, its confirmed cache changes, and the remaining mutations' recovery data together before resolving the call. Saving the read cache separately means a cache write failure cannot block an already confirmed mutation. The receipt and confirmed cache changes remain in the journal until the read cache checkpoint succeeds, then fate removes them. A tab that missed the confirmation recovers its result from the server receipt when it reconnects. This lookup cannot execute a discarded mutation. After a restart, fate repairs those changes before serving saved reads; if storage is still unavailable, reads fall back to the network instead of returning stale saved data.
|
|
260
|
+
|
|
261
|
+
You can also retry delivery or discard a mutation:
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
session.retry();
|
|
265
|
+
await session.discard(mutationId);
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`retry()` respects the saved retry deadline. `discard()` removes an unsent mutation or a terminal failure. A mutation that was already attempted cannot be discarded while its result is unknown: it may have committed remotely, and fate must recover that result first.
|
|
269
|
+
|
|
270
|
+
When leaving an account, dispose its session and unsubscribe:
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
session.dispose();
|
|
274
|
+
unsubscribe();
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Disposing stops that client's delivery, rejects its pending promises, and removes its optimistic updates from memory. Saved mutations remain available to the next client using the same key. Responses arriving after disposal cannot update the old client's cache.
|
|
278
|
+
|
|
279
|
+
## Clearing Saved Data
|
|
280
|
+
|
|
281
|
+
To remove the saved read cache, call `clearCache()`:
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
await fate.persistence!.clearCache();
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
This leaves current in-memory data, queued mutations, and their recovery data intact. Confirmation receipts waiting for a cache checkpoint also remain available; fate removes them after a successful checkpoint.
|
|
288
|
+
|
|
289
|
+
For a complete local account reset, stop every client using the key before removing its journal header, individual mutation entries, and namespaced cache entries from storage. This also removes unsent changes. It cannot undo mutations that already ran on the server.
|
|
290
|
+
|
|
291
|
+
Saved data carries the client's [hydration scope](requests.md#ssr-and-hydration). An incompatible read cache is discarded, while incompatible queued mutations are kept as failures for inspection. Change `hydrationScope` when deploying incompatible cache schemas or changing the meaning of mutation inputs. An unknown or corrupt mutation journal blocks restoration and is left untouched so the saved work can be recovered.
|
|
292
|
+
|
|
293
|
+
## Other Storage Backends
|
|
294
|
+
|
|
295
|
+
The core persistence layer has no IndexedDB dependency. You can use another backend by implementing `PersistenceStorage`:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
interface PersistenceStorage {
|
|
299
|
+
read(key: string): Promise<unknown>;
|
|
300
|
+
write(key: string, value: unknown): Promise<void>;
|
|
301
|
+
scan(
|
|
302
|
+
prefix: string,
|
|
303
|
+
after?: string,
|
|
304
|
+
limit?: number,
|
|
305
|
+
): Promise<Array<{ key: string; value: unknown }>>;
|
|
306
|
+
writeBatch(entries: ReadonlyArray<readonly [string, unknown]>): Promise<void>;
|
|
307
|
+
exclusive<T>(key: string, run: () => Promise<T>): Promise<T>;
|
|
308
|
+
subscribe?(key: string, listener: () => void): () => void;
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Values use fate's hydration codec and can be stored as JSON. The adapter handles storage and coordination:
|
|
313
|
+
|
|
314
|
+
- `read` returns the saved value for a key.
|
|
315
|
+
- `write` replaces one value atomically and resolves after it has committed.
|
|
316
|
+
- `scan` returns keys under a prefix in ascending order, strictly after the optional cursor, up to the limit (64 by default). Use your backend's ordered index to keep each scan small.
|
|
317
|
+
- `writeBatch` commits all entries atomically. An `undefined` value deletes the key.
|
|
318
|
+
- `exclusive` coordinates every tab or process sharing the backend. Different lock names are independent: fate holds a delivery lock during network work and acquires a separate write lock when updating storage. Your adapter must allow that nesting.
|
|
319
|
+
- `subscribe` notifies other clients after a change commits, including keys changed by `writeBatch`. Without notifications, clients check saved mutations during delivery attempts and explicit `retry()` calls.
|
|
320
|
+
|
|
321
|
+
The IndexedDB adapter implements this with `idb`, IndexedDB transactions, Web Locks, and BroadcastChannel. It requires a secure browser context with Web Locks. Browser storage can still be cleared or evicted; if your app needs stronger retention, request persistent browser storage or choose another backend.
|
|
322
|
+
|
|
323
|
+
Persistence coordinates mutation delivery across tabs, but it does not keep every tab's read cache synchronized or resolve application conflicts. Use refetching or [Live Views](live-views.md) to receive fresh server data.
|
|
@@ -136,62 +136,4 @@ const scopeArgsPayload = (args, scope) => {
|
|
|
136
136
|
return result;
|
|
137
137
|
};
|
|
138
138
|
//#endregion
|
|
139
|
-
|
|
140
|
-
var FateRequestError = class extends Error {
|
|
141
|
-
constructor(code, message, options = {}) {
|
|
142
|
-
super(message);
|
|
143
|
-
this.name = "FateRequestError";
|
|
144
|
-
this.code = code;
|
|
145
|
-
this.issues = options.issues;
|
|
146
|
-
this.status = options.status ?? statusFromErrorCode(code);
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
const statusFromErrorCode = (code) => {
|
|
150
|
-
switch (code) {
|
|
151
|
-
case "BAD_REQUEST":
|
|
152
|
-
case "VALIDATION_ERROR": return 400;
|
|
153
|
-
case "UNAUTHORIZED": return 401;
|
|
154
|
-
case "FORBIDDEN": return 403;
|
|
155
|
-
case "NOT_FOUND": return 404;
|
|
156
|
-
case "INTERNAL_ERROR": return 500;
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
const errorCodeFromStatus = (status) => {
|
|
160
|
-
if (status === 400) return "BAD_REQUEST";
|
|
161
|
-
if (status === 401) return "UNAUTHORIZED";
|
|
162
|
-
if (status === 403) return "FORBIDDEN";
|
|
163
|
-
if (status === 404) return "NOT_FOUND";
|
|
164
|
-
return "INTERNAL_ERROR";
|
|
165
|
-
};
|
|
166
|
-
const toProtocolError = (error) => {
|
|
167
|
-
if (error instanceof FateRequestError) return {
|
|
168
|
-
code: error.code,
|
|
169
|
-
issues: error.issues,
|
|
170
|
-
message: error.message
|
|
171
|
-
};
|
|
172
|
-
if (error instanceof Error) return {
|
|
173
|
-
code: "INTERNAL_ERROR",
|
|
174
|
-
message: "Internal server error."
|
|
175
|
-
};
|
|
176
|
-
return {
|
|
177
|
-
code: "INTERNAL_ERROR",
|
|
178
|
-
message: "Internal server error."
|
|
179
|
-
};
|
|
180
|
-
};
|
|
181
|
-
//#endregion
|
|
182
|
-
//#region src/liveTopics.ts
|
|
183
|
-
const encodeTopicPart = (value) => encodeURIComponent(String(value));
|
|
184
|
-
const normalizeConnectionArgs = (args) => {
|
|
185
|
-
const filtered = filterConnectionArgs(args);
|
|
186
|
-
if (!filtered) return;
|
|
187
|
-
const id = filtered.id;
|
|
188
|
-
return typeof id === "string" || typeof id === "number" ? {
|
|
189
|
-
...filtered,
|
|
190
|
-
id: String(id)
|
|
191
|
-
} : filtered;
|
|
192
|
-
};
|
|
193
|
-
const liveEntityTopic = (type, id) => `entity:${encodeTopicPart(type)}:${encodeTopicPart(id)}`;
|
|
194
|
-
const liveConnectionTopic = (procedure, args) => `connection:${encodeTopicPart(procedure)}:${encodeTopicPart(hashArgs(normalizeConnectionArgs(args) ?? {}))}`;
|
|
195
|
-
const liveGlobalConnectionTopic = (procedure) => `connection:${encodeTopicPart(procedure)}:*`;
|
|
196
|
-
//#endregion
|
|
197
|
-
export { errorCodeFromStatus as a, cloneArgs as c, hashArgs as d, paginationArgKeys as f, FateRequestError as i, combineArgsPayload as l, scopeArgsPayload as m, liveEntityTopic as n, toProtocolError as o, resolvedArgsFromPlan as p, liveGlobalConnectionTopic as r, applyArgsPayloadToPlan as s, liveConnectionTopic as t, filterConnectionArgs as u };
|
|
139
|
+
export { hashArgs as a, scopeArgsPayload as c, filterConnectionArgs as i, cloneArgs as n, paginationArgKeys as o, combineArgsPayload as r, resolvedArgsFromPlan as s, applyArgsPayloadToPlan as t };
|
package/lib/cli.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { st as QueryProcedure, v as AnyRecord } from "./transport-DqjhjLfC.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
|
|
4
3
|
//#region src/server/dataView.d.ts
|
|
5
4
|
declare const dataViewFieldsKey: unique symbol;
|
|
6
5
|
declare const dataViewBaseKey: unique symbol;
|
|
@@ -28,7 +27,7 @@ type CountSelection = {
|
|
|
28
27
|
where?: CountWhere;
|
|
29
28
|
};
|
|
30
29
|
type ComputedSelection = CountSelection | FieldSelection;
|
|
31
|
-
type ComputedDeps<Select extends Record<string, ComputedSelection> | undefined> = Select extends Record<string, ComputedSelection> ? { [Key in keyof Select]: Select[Key] extends CountSelection ? number : Select[Key] extends FieldSelection ? unknown : never } : Record<string, unknown>;
|
|
30
|
+
type ComputedDeps<Select extends Record<string, ComputedSelection> | undefined> = Select extends Record<string, ComputedSelection> ? { [Key in keyof Select]: Select[Key] extends CountSelection ? number : Select[Key] extends FieldSelection ? unknown : never; } : Record<string, unknown>;
|
|
32
31
|
type ComputedResolve<Item extends AnyRecord, Result, Select extends Record<string, ComputedSelection> | undefined, Context> = Bivariant<(item: Item, deps: ComputedDeps<Select>, context?: Context, args?: AnyRecord) => Promise<Result> | Result>;
|
|
33
32
|
/**
|
|
34
33
|
* Field configuration for selecting and resolving a computed value on the backend.
|
|
@@ -49,7 +48,7 @@ type DataField<Item extends AnyRecord> = true | ComputedField<Item, any, any, an
|
|
|
49
48
|
/**
|
|
50
49
|
* Recursively serializes resolver results for transport across the network.
|
|
51
50
|
*/
|
|
52
|
-
type Serializable<T> = T extends Date ? string : T extends Array<infer U> ? Array<Serializable<U>> : T extends object ? { [K in keyof T]: Serializable<T[K]
|
|
51
|
+
type Serializable<T> = T extends Date ? string : T extends Array<infer U> ? Array<Serializable<U>> : T extends object ? { [K in keyof T]: Serializable<T[K]>; } : T;
|
|
53
52
|
/**
|
|
54
53
|
* Server-side mirror of a view definition describing how to select and resolve
|
|
55
54
|
* fields when fulfilling a client request.
|
|
@@ -89,7 +88,7 @@ declare function dataView<Item extends AnyRecord>(typeName: string): <Fields ext
|
|
|
89
88
|
* connection information.
|
|
90
89
|
*/
|
|
91
90
|
declare const list: <Item extends AnyRecord>(view: DataView<Item>, options?: DataViewListOptions) => DataView<Item> & DataViewInternals<Item> & {
|
|
92
|
-
kind:
|
|
91
|
+
kind: 'list';
|
|
93
92
|
};
|
|
94
93
|
/**
|
|
95
94
|
* Declares a resolver field inside a data view, optionally providing a
|
|
@@ -118,7 +117,7 @@ type ViewFieldConfig<V extends DataView<AnyRecord>> = V extends {
|
|
|
118
117
|
readonly [dataViewFieldsKey]: infer Fields;
|
|
119
118
|
} ? Fields : V['fields'];
|
|
120
119
|
type RawFieldResult<Item extends AnyRecord, Key extends PropertyKey, Field extends DataField<Item>> = Field extends true ? Key extends keyof Item ? Item[Key] : never : Field extends DataView<infer ChildItem> ? Key extends keyof Item ? RelationResult<Item[Key], DataView<ChildItem>> : never : Field extends ComputedField<Item, any, any, any> ? ComputedResult<Field> : Field extends ResolverField<Item, any, any> ? ResolverResult<Field> : never;
|
|
121
|
-
type RawDataViewResult<V extends DataView<AnyRecord>> = V extends DataView<infer Item> ? { [K in keyof ViewFieldConfig<V>]: RawFieldResult<Item, K, ViewFieldConfig<V>[K]
|
|
120
|
+
type RawDataViewResult<V extends DataView<AnyRecord>> = V extends DataView<infer Item> ? { [K in keyof ViewFieldConfig<V>]: RawFieldResult<Item, K, ViewFieldConfig<V>[K]>; } : never;
|
|
122
121
|
type DataViewResult<V extends DataView<AnyRecord>> = Serializable<RawDataViewResult<V>>;
|
|
123
122
|
type WithTypename<T, Name extends string> = T & {
|
|
124
123
|
__typename: Name;
|
|
@@ -150,12 +149,7 @@ type ViewPlan<Item extends AnyRecord = AnyRecord, Context = unknown> = {
|
|
|
150
149
|
* Builds a resolver that applies a client's selection to a server data view,
|
|
151
150
|
* filtering fields, running nested resolvers, and shaping Prisma selects.
|
|
152
151
|
*/
|
|
153
|
-
declare function createResolver<Item extends AnyRecord, Context = unknown>({
|
|
154
|
-
args,
|
|
155
|
-
ctx,
|
|
156
|
-
select,
|
|
157
|
-
view
|
|
158
|
-
}: {
|
|
152
|
+
declare function createResolver<Item extends AnyRecord, Context = unknown>({ args, ctx, select, view }: {
|
|
159
153
|
args?: AnyRecord;
|
|
160
154
|
ctx?: Context;
|
|
161
155
|
select: Iterable<string>;
|
|
@@ -172,7 +166,7 @@ declare function createResolver<Item extends AnyRecord, Context = unknown>({
|
|
|
172
166
|
type ConnectionInput = z.infer<typeof connectionInput>;
|
|
173
167
|
type AdditionalInputSchema = z.ZodObject<Record<string, z.ZodTypeAny>>;
|
|
174
168
|
type ConnectionInputWithAdditional<TAdditionalInput extends AdditionalInputSchema | undefined> = ConnectionInput & {
|
|
175
|
-
args?: ConnectionInput['args'] extends infer A ? A & (TAdditionalInput extends AdditionalInputSchema ? z.infer<TAdditionalInput> : object) : never;
|
|
169
|
+
args?: ConnectionInput['args'] extends (infer A) ? A & (TAdditionalInput extends AdditionalInputSchema ? z.infer<TAdditionalInput> : object) : never;
|
|
176
170
|
};
|
|
177
171
|
type ConnectionCursor = string;
|
|
178
172
|
/**
|
|
@@ -314,32 +308,22 @@ type SourcePlan<Item extends AnyRecord = AnyRecord, Context = unknown, Adapter =
|
|
|
314
308
|
type DataViewModule = Record<string, unknown>;
|
|
315
309
|
declare const hasNestedSelection: (select: Iterable<string>, field: string) => boolean;
|
|
316
310
|
declare const getNestedSelection: (select: Iterable<string>, field: string) => Array<string>;
|
|
317
|
-
declare function createSourcePlan<Item extends AnyRecord, Context = unknown, Adapter = unknown>({
|
|
318
|
-
args,
|
|
319
|
-
ctx,
|
|
320
|
-
select,
|
|
321
|
-
source
|
|
322
|
-
}: {
|
|
311
|
+
declare function createSourcePlan<Item extends AnyRecord, Context = unknown, Adapter = unknown>({ args, ctx, select, source }: {
|
|
323
312
|
args?: Record<string, unknown>;
|
|
324
313
|
ctx?: Context;
|
|
325
314
|
select: Iterable<string>;
|
|
326
315
|
source: SourceDefinition<Item, Adapter>;
|
|
327
316
|
}): {
|
|
328
|
-
root: SourcePlanNode<unknown, Adapter>;
|
|
329
|
-
source: SourceDefinition<Item, Adapter>;
|
|
330
317
|
args: AnyRecord | undefined;
|
|
331
318
|
ctx: Context | undefined;
|
|
332
319
|
resolve: (item: Item) => Promise<AnyRecord>;
|
|
333
320
|
resolveMany: (items: Array<AnyRecord>) => Promise<Array<AnyRecord>>;
|
|
334
321
|
selectedPaths: Set<string>;
|
|
335
322
|
view: DataView<Item>;
|
|
323
|
+
root: SourcePlanNode<unknown, Adapter>;
|
|
324
|
+
source: SourceDefinition<Item, Adapter>;
|
|
336
325
|
};
|
|
337
|
-
declare function createNestedSourcePlan<Item extends AnyRecord, Context = unknown, Adapter = unknown>({
|
|
338
|
-
ctx,
|
|
339
|
-
field,
|
|
340
|
-
input,
|
|
341
|
-
source
|
|
342
|
-
}: {
|
|
326
|
+
declare function createNestedSourcePlan<Item extends AnyRecord, Context = unknown, Adapter = unknown>({ ctx, field, input, source }: {
|
|
343
327
|
ctx?: Context;
|
|
344
328
|
field: string;
|
|
345
329
|
input: {
|
|
@@ -348,14 +332,14 @@ declare function createNestedSourcePlan<Item extends AnyRecord, Context = unknow
|
|
|
348
332
|
};
|
|
349
333
|
source: SourceDefinition<Item, Adapter>;
|
|
350
334
|
}): {
|
|
351
|
-
root: SourcePlanNode<unknown, Adapter>;
|
|
352
|
-
source: SourceDefinition<Item, Adapter>;
|
|
353
335
|
args: AnyRecord | undefined;
|
|
354
336
|
ctx: Context | undefined;
|
|
355
337
|
resolve: (item: Item) => Promise<AnyRecord>;
|
|
356
338
|
resolveMany: (items: Array<AnyRecord>) => Promise<Array<AnyRecord>>;
|
|
357
339
|
selectedPaths: Set<string>;
|
|
358
340
|
view: DataView<Item>;
|
|
341
|
+
root: SourcePlanNode<unknown, Adapter>;
|
|
342
|
+
source: SourceDefinition<Item, Adapter>;
|
|
359
343
|
};
|
|
360
344
|
//#endregion
|
|
361
345
|
//#region src/server/executor.d.ts
|
|
@@ -386,14 +370,7 @@ type SourceExecutor<Context, Item extends AnyRecord = AnyRecord, ByIdExtra = unk
|
|
|
386
370
|
connection?: SourceConnectionHandler<Context, ConnectionExtra, Item>;
|
|
387
371
|
};
|
|
388
372
|
type SourceRegistry<Context> = Map<SourceDefinition<AnyRecord, unknown>, SourceExecutor<Context, AnyRecord, any, any, any>>;
|
|
389
|
-
declare const resolveSourceByIds: <Context, Item extends AnyRecord>({
|
|
390
|
-
ctx,
|
|
391
|
-
extra,
|
|
392
|
-
ids,
|
|
393
|
-
input,
|
|
394
|
-
registry,
|
|
395
|
-
source
|
|
396
|
-
}: {
|
|
373
|
+
declare const resolveSourceByIds: <Context, Item extends AnyRecord>({ ctx, extra, ids, input, registry, source }: {
|
|
397
374
|
ctx: Context;
|
|
398
375
|
extra?: unknown;
|
|
399
376
|
ids: Array<string>;
|
|
@@ -404,14 +381,7 @@ declare const resolveSourceByIds: <Context, Item extends AnyRecord>({
|
|
|
404
381
|
registry: SourceRegistry<Context>;
|
|
405
382
|
source: SourceDefinition<Item, unknown>;
|
|
406
383
|
}) => Promise<AnyRecord[]>;
|
|
407
|
-
declare const resolveSourceById: <Context, Item extends AnyRecord>({
|
|
408
|
-
ctx,
|
|
409
|
-
extra,
|
|
410
|
-
id,
|
|
411
|
-
input,
|
|
412
|
-
registry,
|
|
413
|
-
source
|
|
414
|
-
}: {
|
|
384
|
+
declare const resolveSourceById: <Context, Item extends AnyRecord>({ ctx, extra, id, input, registry, source }: {
|
|
415
385
|
ctx: Context;
|
|
416
386
|
extra?: unknown;
|
|
417
387
|
id: string;
|
|
@@ -422,20 +392,10 @@ declare const resolveSourceById: <Context, Item extends AnyRecord>({
|
|
|
422
392
|
registry: SourceRegistry<Context>;
|
|
423
393
|
source: SourceDefinition<Item, unknown>;
|
|
424
394
|
}) => Promise<AnyRecord | null>;
|
|
425
|
-
declare const resolveSourceConnection: <Context, Item extends AnyRecord>({
|
|
426
|
-
ctx,
|
|
427
|
-
cursor,
|
|
428
|
-
direction,
|
|
429
|
-
extra,
|
|
430
|
-
input,
|
|
431
|
-
registry,
|
|
432
|
-
skip,
|
|
433
|
-
source,
|
|
434
|
-
take
|
|
435
|
-
}: {
|
|
395
|
+
declare const resolveSourceConnection: <Context, Item extends AnyRecord>({ ctx, cursor, direction, extra, input, registry, skip, source, take }: {
|
|
436
396
|
ctx: Context;
|
|
437
397
|
cursor?: string;
|
|
438
|
-
direction:
|
|
398
|
+
direction: 'backward' | 'forward';
|
|
439
399
|
extra?: unknown;
|
|
440
400
|
input: {
|
|
441
401
|
args?: Record<string, unknown>;
|
|
@@ -446,14 +406,7 @@ declare const resolveSourceConnection: <Context, Item extends AnyRecord>({
|
|
|
446
406
|
source: SourceDefinition<Item, unknown>;
|
|
447
407
|
take: number;
|
|
448
408
|
}) => Promise<AnyRecord[]>;
|
|
449
|
-
declare const refetchSourceById: <Context, Item extends AnyRecord>({
|
|
450
|
-
ctx,
|
|
451
|
-
extra,
|
|
452
|
-
id,
|
|
453
|
-
input,
|
|
454
|
-
registry,
|
|
455
|
-
source
|
|
456
|
-
}: {
|
|
409
|
+
declare const refetchSourceById: <Context, Item extends AnyRecord>({ ctx, extra, id, input, registry, source }: {
|
|
457
410
|
ctx: Context;
|
|
458
411
|
extra?: unknown;
|
|
459
412
|
id: string;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { D as Entity, G as TypeConfig, R as MutationShape, t as Transport } from "./transport-DqjhjLfC.mjs";
|
|
3
2
|
//#region src/graphqlTransport.d.ts
|
|
4
3
|
type TransportMutations = Record<string, MutationShape>;
|
|
5
4
|
type EmptyTransportMutations = Record<never, MutationShape>;
|
|
@@ -31,7 +30,7 @@ type GraphQLMutationOutput<Definition> = Definition extends GraphQLMutationDefin
|
|
|
31
30
|
type GraphQLMutationMap<Mutations> = Mutations extends Record<string, GraphQLMutationDefinition> ? { [K in keyof Mutations]: {
|
|
32
31
|
input: GraphQLMutationInput<Mutations[K]>;
|
|
33
32
|
output: GraphQLMutationOutput<Mutations[K]>;
|
|
34
|
-
} } : EmptyTransportMutations;
|
|
33
|
+
}; } : EmptyTransportMutations;
|
|
35
34
|
type GraphQLMutationRuntimeConfig = {
|
|
36
35
|
entity: string;
|
|
37
36
|
field: string;
|
|
@@ -50,6 +49,7 @@ type GraphQLTransportOptions<Mutations extends TransportMutations = EmptyTranspo
|
|
|
50
49
|
fetch?: FetchLike;
|
|
51
50
|
headers?: HeadersFactory;
|
|
52
51
|
live?: boolean | GraphQLLiveOptions;
|
|
52
|
+
mutateDurably?: Transport<Mutations>['mutateDurably'];
|
|
53
53
|
mutations?: Record<Extract<keyof Mutations, string>, GraphQLMutationRuntimeConfig>;
|
|
54
54
|
roots?: Record<string, GraphQLRootConfig>;
|
|
55
55
|
types: ReadonlyArray<Omit<TypeConfig, 'getId'> & Partial<Pick<TypeConfig, 'getId'>>>;
|
|
@@ -59,16 +59,6 @@ declare function graphqlMutation<T extends Entity, Input, Output>(entity: T['__t
|
|
|
59
59
|
field: string;
|
|
60
60
|
inputArg?: false | string;
|
|
61
61
|
}): GraphQLMutationDefinition<T, Input, Output>;
|
|
62
|
-
declare function createGraphQLTransport<Mutations extends TransportMutations = EmptyTransportMutations>({
|
|
63
|
-
decodeNodeId,
|
|
64
|
-
encodeNodeId,
|
|
65
|
-
fetch: fetchImpl,
|
|
66
|
-
headers,
|
|
67
|
-
live,
|
|
68
|
-
mutations,
|
|
69
|
-
roots,
|
|
70
|
-
types: typeConfigs,
|
|
71
|
-
url
|
|
72
|
-
}: GraphQLTransportOptions<Mutations>): Transport<Mutations>;
|
|
62
|
+
declare function createGraphQLTransport<Mutations extends TransportMutations = EmptyTransportMutations>({ decodeNodeId, encodeNodeId, fetch: fetchImpl, headers, live, mutateDurably, mutations, roots, types: typeConfigs, url }: GraphQLTransportOptions<Mutations>): Transport<Mutations>;
|
|
73
63
|
//#endregion
|
|
74
64
|
export { GraphQLTransportOptions as a, GraphQLMutationOutput as i, GraphQLMutationInput as n, createGraphQLTransport as o, GraphQLMutationMap as r, graphqlMutation as s, GraphQLMutationDefinition as t };
|
|
@@ -230,7 +230,7 @@ function graphqlMutation(entity, options) {
|
|
|
230
230
|
inputArg: options.inputArg
|
|
231
231
|
});
|
|
232
232
|
}
|
|
233
|
-
function createGraphQLTransport({ decodeNodeId = defaultDecodeNodeId, encodeNodeId = defaultEncodeNodeId, fetch: fetchImpl = defaultFetch, headers, live = true, mutations, roots, types: typeConfigs, url }) {
|
|
233
|
+
function createGraphQLTransport({ decodeNodeId = defaultDecodeNodeId, encodeNodeId = defaultEncodeNodeId, fetch: fetchImpl = defaultFetch, headers, live = true, mutateDurably, mutations, roots, types: typeConfigs, url }) {
|
|
234
234
|
const endpoint = normalizeEndpoint(url);
|
|
235
235
|
const types = new Map(typeConfigs.map((type) => [type.type, type]));
|
|
236
236
|
let nextId = 0;
|
|
@@ -387,7 +387,8 @@ function createGraphQLTransport({ decodeNodeId = defaultDecodeNodeId, encodeNode
|
|
|
387
387
|
value
|
|
388
388
|
})
|
|
389
389
|
});
|
|
390
|
-
}
|
|
390
|
+
},
|
|
391
|
+
mutateDurably
|
|
391
392
|
};
|
|
392
393
|
if (live !== false) {
|
|
393
394
|
const liveOptions = typeof live === "object" ? live : {};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as GraphQLTransportOptions, i as GraphQLMutationOutput, n as GraphQLMutationInput, o as createGraphQLTransport, r as GraphQLMutationMap, s as graphqlMutation, t as GraphQLMutationDefinition } from "./graphqlTransport-
|
|
1
|
+
import { a as GraphQLTransportOptions, i as GraphQLMutationOutput, n as GraphQLMutationInput, o as createGraphQLTransport, r as GraphQLMutationMap, s as graphqlMutation, t as GraphQLMutationDefinition } from "./graphqlTransport-DGLYyRxL.mjs";
|
|
2
2
|
export { GraphQLMutationDefinition, GraphQLMutationInput, GraphQLMutationMap, GraphQLMutationOutput, GraphQLTransportOptions, createGraphQLTransport, graphqlMutation };
|
package/lib/graphqlTransport.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as graphqlMutation, t as createGraphQLTransport } from "./graphqlTransport-
|
|
1
|
+
import { n as graphqlMutation, t as createGraphQLTransport } from "./graphqlTransport-DmheG3AO.mjs";
|
|
2
2
|
export { createGraphQLTransport, graphqlMutation };
|