houdini-svelte 0.17.0-next.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/package.json +57 -0
- package/src/plugin/codegen/adapter.ts +45 -0
- package/src/plugin/codegen/components/index.ts +149 -0
- package/src/plugin/codegen/index.ts +28 -0
- package/src/plugin/codegen/routes/index.ts +307 -0
- package/src/plugin/codegen/routes/kit.test.ts +276 -0
- package/src/plugin/codegen/stores/fragment.ts +83 -0
- package/src/plugin/codegen/stores/index.ts +55 -0
- package/src/plugin/codegen/stores/mutation.ts +56 -0
- package/src/plugin/codegen/stores/query.test.ts +504 -0
- package/src/plugin/codegen/stores/query.ts +97 -0
- package/src/plugin/codegen/stores/subscription.ts +57 -0
- package/src/plugin/extract.test.ts +290 -0
- package/src/plugin/extract.ts +127 -0
- package/src/plugin/extractLoadFunction.test.ts +247 -0
- package/src/plugin/extractLoadFunction.ts +249 -0
- package/src/plugin/fsPatch.ts +238 -0
- package/src/plugin/imports.ts +28 -0
- package/src/plugin/index.ts +165 -0
- package/src/plugin/kit.ts +382 -0
- package/src/plugin/transforms/index.ts +90 -0
- package/src/plugin/transforms/kit/index.ts +20 -0
- package/src/plugin/transforms/kit/init.test.ts +28 -0
- package/src/plugin/transforms/kit/init.ts +75 -0
- package/src/plugin/transforms/kit/load.test.ts +1234 -0
- package/src/plugin/transforms/kit/load.ts +506 -0
- package/src/plugin/transforms/kit/session.test.ts +268 -0
- package/src/plugin/transforms/kit/session.ts +161 -0
- package/src/plugin/transforms/query.test.ts +99 -0
- package/src/plugin/transforms/query.ts +263 -0
- package/src/plugin/transforms/reactive.ts +126 -0
- package/src/plugin/transforms/tags.ts +20 -0
- package/src/plugin/transforms/types.ts +9 -0
- package/src/plugin/validate.test.ts +95 -0
- package/src/plugin/validate.ts +50 -0
- package/src/preprocess/index.ts +33 -0
- package/src/runtime/adapter.ts +21 -0
- package/src/runtime/fragments.ts +86 -0
- package/src/runtime/index.ts +72 -0
- package/src/runtime/network.ts +6 -0
- package/src/runtime/session.ts +187 -0
- package/src/runtime/stores/fragment.ts +48 -0
- package/src/runtime/stores/index.ts +5 -0
- package/src/runtime/stores/mutation.ts +185 -0
- package/src/runtime/stores/pagination/cursor.ts +265 -0
- package/src/runtime/stores/pagination/fetch.ts +7 -0
- package/src/runtime/stores/pagination/fragment.ts +236 -0
- package/src/runtime/stores/pagination/index.ts +7 -0
- package/src/runtime/stores/pagination/offset.ts +162 -0
- package/src/runtime/stores/pagination/pageInfo.test.ts +39 -0
- package/src/runtime/stores/pagination/pageInfo.ts +67 -0
- package/src/runtime/stores/pagination/query.ts +132 -0
- package/src/runtime/stores/query.ts +524 -0
- package/src/runtime/stores/store.ts +13 -0
- package/src/runtime/stores/subscription.ts +107 -0
- package/src/runtime/types.ts +40 -0
- package/src/test/index.ts +208 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { QueryStore } from './stores'
|
|
2
|
+
|
|
3
|
+
export * from './adapter'
|
|
4
|
+
export * from './stores'
|
|
5
|
+
export * from './fragments'
|
|
6
|
+
export * from './session'
|
|
7
|
+
|
|
8
|
+
type LoadResult = Promise<{ [key: string]: QueryStore<any, {}> }>
|
|
9
|
+
type LoadAllInput = LoadResult | Record<string, LoadResult>
|
|
10
|
+
|
|
11
|
+
// putting this here was the only way i could find to reliably avoid import issues
|
|
12
|
+
// its really the only thing from lib that users should import so it makes sense to have it here....
|
|
13
|
+
export async function loadAll(
|
|
14
|
+
...loads: LoadAllInput[]
|
|
15
|
+
): Promise<Record<string, QueryStore<any, {}>>> {
|
|
16
|
+
// we need to collect all of the promises in a single list that we will await in promise.all and then build up
|
|
17
|
+
const promises: LoadResult[] = []
|
|
18
|
+
|
|
19
|
+
// the question we have to answer is whether entry is a promise or an object of promises
|
|
20
|
+
const isPromise = (val: LoadAllInput): val is LoadResult =>
|
|
21
|
+
'then' in val && 'finally' in val && 'catch' in val
|
|
22
|
+
|
|
23
|
+
for (const entry of loads) {
|
|
24
|
+
if (!isPromise(entry) && 'then' in entry) {
|
|
25
|
+
throw new Error('❌ `then` is not a valid key for an object passed to loadAll')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// identify an entry with the `.then` method
|
|
29
|
+
if (isPromise(entry)) {
|
|
30
|
+
promises.push(entry)
|
|
31
|
+
} else {
|
|
32
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
33
|
+
if (isPromise(value)) {
|
|
34
|
+
promises.push(value)
|
|
35
|
+
} else {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`❌ ${key} is not a valid value for an object passed to loadAll. You must pass the result of a load_Store function`
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// now that we've collected all of the promises, wait for them
|
|
45
|
+
await Promise.all(promises)
|
|
46
|
+
|
|
47
|
+
// all of the promises are resolved so go back over the value we were given a reconstruct it
|
|
48
|
+
let result = {}
|
|
49
|
+
|
|
50
|
+
for (const entry of loads) {
|
|
51
|
+
// if we're looking at a promise, it will contain the key
|
|
52
|
+
if (isPromise(entry)) {
|
|
53
|
+
Object.assign(result, await entry)
|
|
54
|
+
} else {
|
|
55
|
+
Object.assign(
|
|
56
|
+
result,
|
|
57
|
+
// await every value in the object and assign it to result
|
|
58
|
+
Object.fromEntries(
|
|
59
|
+
await Promise.all(
|
|
60
|
+
Object.entries(entry).map(async ([key, value]) => [
|
|
61
|
+
key,
|
|
62
|
+
Object.values(await value)[0],
|
|
63
|
+
])
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// we're done
|
|
71
|
+
return result
|
|
72
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { marshalInputs } from '$houdini/runtime/lib/scalars'
|
|
2
|
+
import {
|
|
3
|
+
MutationArtifact,
|
|
4
|
+
QueryArtifact,
|
|
5
|
+
QueryResult,
|
|
6
|
+
SubscriptionArtifact,
|
|
7
|
+
} from '$houdini/runtime/lib/types'
|
|
8
|
+
import { error, LoadEvent, redirect, RequestEvent } from '@sveltejs/kit'
|
|
9
|
+
import { GraphQLError } from 'graphql'
|
|
10
|
+
import { get } from 'svelte/store'
|
|
11
|
+
|
|
12
|
+
import { isBrowser } from './adapter'
|
|
13
|
+
import { AfterLoadArgs, BeforeLoadArgs, OnErrorArgs } from './types'
|
|
14
|
+
|
|
15
|
+
const sessionKeyName = '__houdini__session__'
|
|
16
|
+
|
|
17
|
+
export class RequestContext {
|
|
18
|
+
private loadEvent: LoadEvent
|
|
19
|
+
continue: boolean = true
|
|
20
|
+
returnValue: {} = {}
|
|
21
|
+
|
|
22
|
+
constructor(ctx: LoadEvent) {
|
|
23
|
+
this.loadEvent = ctx
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
error(status: number, message: string | Error): any {
|
|
27
|
+
throw error(status, typeof message === 'string' ? message : message.message)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
redirect(status: number, location: string): any {
|
|
31
|
+
throw redirect(status, location)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fetch(input: RequestInfo, init?: RequestInit) {
|
|
35
|
+
// make sure to bind the window object to the fetch in a browser
|
|
36
|
+
const fetch =
|
|
37
|
+
typeof window !== 'undefined' ? this.loadEvent.fetch.bind(window) : this.loadEvent.fetch
|
|
38
|
+
|
|
39
|
+
return fetch(input, init)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
graphqlErrors(payload: { errors?: GraphQLError[] }) {
|
|
43
|
+
// if we have a list of errors
|
|
44
|
+
if (payload.errors) {
|
|
45
|
+
return this.error(500, payload.errors.map(({ message }) => message).join('\n'))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return this.error(500, 'Encountered invalid response: ' + JSON.stringify(payload))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// This hook fires before executing any queries, it allows custom props to be passed to the component.
|
|
52
|
+
async invokeLoadHook({
|
|
53
|
+
variant,
|
|
54
|
+
hookFn,
|
|
55
|
+
input,
|
|
56
|
+
data,
|
|
57
|
+
error,
|
|
58
|
+
}: {
|
|
59
|
+
variant: 'before' | 'after' | 'error'
|
|
60
|
+
hookFn: KitBeforeLoad | KitAfterLoad | KitOnError
|
|
61
|
+
input: Record<string, any>
|
|
62
|
+
data: Record<string, any>
|
|
63
|
+
error: unknown
|
|
64
|
+
}) {
|
|
65
|
+
// call the onLoad function to match the framework
|
|
66
|
+
let hookCall
|
|
67
|
+
if (variant === 'before') {
|
|
68
|
+
hookCall = (hookFn as KitBeforeLoad).call(this, this.loadEvent as BeforeLoadArgs)
|
|
69
|
+
} else if (variant === 'after') {
|
|
70
|
+
// we have to assign input and data onto load so that we don't read values that
|
|
71
|
+
// are deprecated and generate warnings when read
|
|
72
|
+
hookCall = (hookFn as KitAfterLoad).call(this, {
|
|
73
|
+
event: this.loadEvent,
|
|
74
|
+
input,
|
|
75
|
+
data: Object.fromEntries(
|
|
76
|
+
Object.entries(data).map(([key, store]) => [
|
|
77
|
+
key,
|
|
78
|
+
get<QueryResult<any, any>>(store).data,
|
|
79
|
+
])
|
|
80
|
+
),
|
|
81
|
+
} as AfterLoadArgs)
|
|
82
|
+
} else if (variant === 'error') {
|
|
83
|
+
hookCall = (hookFn as KitOnError).call(this, {
|
|
84
|
+
event: this.loadEvent,
|
|
85
|
+
input,
|
|
86
|
+
error,
|
|
87
|
+
} as OnErrorArgs)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// make sure any promises are resolved
|
|
91
|
+
let result = await hookCall
|
|
92
|
+
|
|
93
|
+
// If the returnValue is already set through this.error or this.redirect return early
|
|
94
|
+
if (!this.continue) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
// If the result is null or undefined, or the result isn't an object return early
|
|
98
|
+
if (result == null || typeof result !== 'object') {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.returnValue = result
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// compute the inputs for an operation should reflect the framework's conventions.
|
|
106
|
+
async computeInput({
|
|
107
|
+
variableFunction,
|
|
108
|
+
artifact,
|
|
109
|
+
}: {
|
|
110
|
+
variableFunction: KitBeforeLoad
|
|
111
|
+
artifact: QueryArtifact | MutationArtifact | SubscriptionArtifact
|
|
112
|
+
}) {
|
|
113
|
+
// call the variable function to match the framework
|
|
114
|
+
let input = await variableFunction.call(this, this.loadEvent)
|
|
115
|
+
|
|
116
|
+
return await marshalInputs({ artifact, input })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type KitBeforeLoad = (ctx: BeforeLoadArgs) => Record<string, any> | Promise<Record<string, any>>
|
|
121
|
+
type KitAfterLoad = (ctx: AfterLoadArgs) => Record<string, any>
|
|
122
|
+
type KitOnError = (ctx: OnErrorArgs) => Record<string, any>
|
|
123
|
+
|
|
124
|
+
const sessionSentinel = {}
|
|
125
|
+
// @ts-ignore
|
|
126
|
+
let session: App.Session | {} = sessionSentinel
|
|
127
|
+
|
|
128
|
+
export function extractSession(val: {
|
|
129
|
+
[sessionKeyName]: // @ts-ignore
|
|
130
|
+
App.Session
|
|
131
|
+
}) {
|
|
132
|
+
return val[sessionKeyName]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function buildSessionObject(event: RequestEvent) {
|
|
136
|
+
return {
|
|
137
|
+
[sessionKeyName]: extractSession(event.locals as any),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function setClientSession(
|
|
142
|
+
// @ts-ignore
|
|
143
|
+
val: App.Session
|
|
144
|
+
) {
|
|
145
|
+
if (!isBrowser) {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
session = val
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// @ts-ignore
|
|
153
|
+
export function getClientSession(): App.Session {
|
|
154
|
+
return session
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function setSession(
|
|
158
|
+
event: RequestEvent,
|
|
159
|
+
session: // @ts-ignore
|
|
160
|
+
App.Session
|
|
161
|
+
) {
|
|
162
|
+
;(event.locals as any)[sessionKeyName] = session
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function getSession(event?: RequestEvent | LoadEvent): Promise<
|
|
166
|
+
| {}
|
|
167
|
+
// @ts-ignore
|
|
168
|
+
| App.Session
|
|
169
|
+
> {
|
|
170
|
+
if (event) {
|
|
171
|
+
// get the session either from the server side event or the client side event
|
|
172
|
+
if ('locals' in event) {
|
|
173
|
+
// this is a server side event (RequestEvent) -> extract the session from locals
|
|
174
|
+
return extractSession(event.locals as any) || sessionSentinel
|
|
175
|
+
}
|
|
176
|
+
// the session data could also already be present in the data field
|
|
177
|
+
else if ('data' in event && event.data && sessionKeyName in event.data) {
|
|
178
|
+
// @ts-ignore
|
|
179
|
+
return extractSession(event.data) || sessionSentinel
|
|
180
|
+
} else {
|
|
181
|
+
// this is a client side event -> await the parent data which include the session
|
|
182
|
+
return extractSession((await event.parent()) as any) || sessionSentinel
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return session
|
|
187
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CompiledFragmentKind,
|
|
3
|
+
GraphQLObject,
|
|
4
|
+
FragmentArtifact,
|
|
5
|
+
HoudiniFetchContext,
|
|
6
|
+
} from '$houdini/runtime/lib/types'
|
|
7
|
+
import { Writable, writable } from 'svelte/store'
|
|
8
|
+
import type { Readable } from 'svelte/store'
|
|
9
|
+
|
|
10
|
+
import { BaseStore } from './store'
|
|
11
|
+
|
|
12
|
+
// a fragment store exists in multiple places in a given application so we
|
|
13
|
+
// can't just return a store directly, the user has to load the version of the
|
|
14
|
+
// fragment store for the object the store has been mixed into
|
|
15
|
+
export class FragmentStore<
|
|
16
|
+
_Data extends GraphQLObject,
|
|
17
|
+
_Input = {},
|
|
18
|
+
_ExtraFields = {}
|
|
19
|
+
> extends BaseStore {
|
|
20
|
+
artifact: FragmentArtifact
|
|
21
|
+
name: string
|
|
22
|
+
kind = CompiledFragmentKind
|
|
23
|
+
|
|
24
|
+
protected context: HoudiniFetchContext | null = null
|
|
25
|
+
|
|
26
|
+
constructor({ artifact, storeName }: { artifact: FragmentArtifact; storeName: string }) {
|
|
27
|
+
super()
|
|
28
|
+
this.artifact = artifact
|
|
29
|
+
this.name = storeName
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get(initialValue: _Data | null) {
|
|
33
|
+
// at the moment a fragment store doesn't really do anything
|
|
34
|
+
// but we're going to keep it wrapped in a store so we can eventually
|
|
35
|
+
// optimize the updates
|
|
36
|
+
let store = writable(initialValue) as Writable<(_Data | null) & _ExtraFields>
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
kind: CompiledFragmentKind,
|
|
40
|
+
subscribe: (
|
|
41
|
+
...args: Parameters<Readable<(_Data | null) & _ExtraFields>['subscribe']>
|
|
42
|
+
) => {
|
|
43
|
+
return store.subscribe(...args)
|
|
44
|
+
},
|
|
45
|
+
update: (val: (_Data | null) & _ExtraFields) => store?.set(val),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { getCache } from '$houdini/runtime'
|
|
2
|
+
import { executeQuery } from '$houdini/runtime/lib/network'
|
|
3
|
+
import { marshalInputs, marshalSelection, unmarshalSelection } from '$houdini/runtime/lib/scalars'
|
|
4
|
+
import type { SubscriptionSpec, MutationArtifact } from '$houdini/runtime/lib/types'
|
|
5
|
+
import { GraphQLObject } from '$houdini/runtime/lib/types'
|
|
6
|
+
import { Readable } from 'svelte/store'
|
|
7
|
+
import { Writable, writable } from 'svelte/store'
|
|
8
|
+
|
|
9
|
+
import { getCurrentClient } from '../network'
|
|
10
|
+
import { getSession } from '../session'
|
|
11
|
+
import { BaseStore } from './store'
|
|
12
|
+
|
|
13
|
+
export class MutationStore<
|
|
14
|
+
_Data extends GraphQLObject,
|
|
15
|
+
_Input extends {},
|
|
16
|
+
_Optimistic extends GraphQLObject
|
|
17
|
+
> extends BaseStore {
|
|
18
|
+
artifact: MutationArtifact
|
|
19
|
+
kind = 'HoudiniMutation' as const
|
|
20
|
+
|
|
21
|
+
private store: Writable<MutationResult<_Data, _Input>>
|
|
22
|
+
|
|
23
|
+
constructor({ artifact }: { artifact: MutationArtifact }) {
|
|
24
|
+
super()
|
|
25
|
+
this.artifact = artifact
|
|
26
|
+
this.store = writable(this.nullState)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async mutate(
|
|
30
|
+
variables: _Input,
|
|
31
|
+
{
|
|
32
|
+
metadata,
|
|
33
|
+
fetch,
|
|
34
|
+
...mutationConfig
|
|
35
|
+
}: {
|
|
36
|
+
// @ts-ignore
|
|
37
|
+
metadata?: App.Metadata
|
|
38
|
+
fetch?: typeof globalThis.fetch
|
|
39
|
+
} & MutationConfig<_Data, _Input, _Optimistic> = {}
|
|
40
|
+
): Promise<_Data> {
|
|
41
|
+
const cache = getCache()
|
|
42
|
+
const config = await this.getConfig()
|
|
43
|
+
|
|
44
|
+
this.store.update((c) => {
|
|
45
|
+
return { ...c, isFetching: true }
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// treat a mutation like it has an optimistic layer regardless of
|
|
49
|
+
// whether there actually _is_ one. This ensures that a query which fires
|
|
50
|
+
// after this mutation has been sent will overwrite any return values from the mutation
|
|
51
|
+
//
|
|
52
|
+
// as far as I can tell, this is an arbitrary decision but it does give a
|
|
53
|
+
// well-defined ordering to a subtle situation so that seems like a win
|
|
54
|
+
//
|
|
55
|
+
const layer = cache._internal_unstable.storage.createLayer(true)
|
|
56
|
+
|
|
57
|
+
// if there is an optimistic response then we need to write the value immediately
|
|
58
|
+
const optimisticResponse = mutationConfig?.optimisticResponse
|
|
59
|
+
// hold onto the list of subscribers that we updated because of the optimistic response
|
|
60
|
+
// and make sure they are included in the final set of subscribers to notify
|
|
61
|
+
let toNotify: SubscriptionSpec[] = []
|
|
62
|
+
if (optimisticResponse) {
|
|
63
|
+
toNotify = cache.write({
|
|
64
|
+
selection: this.artifact.selection,
|
|
65
|
+
// make sure that any scalar values get processed into something we can cache
|
|
66
|
+
data: (await marshalSelection({
|
|
67
|
+
selection: this.artifact.selection,
|
|
68
|
+
data: optimisticResponse,
|
|
69
|
+
}))!,
|
|
70
|
+
variables,
|
|
71
|
+
layer: layer.id,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const newVariables = (await marshalInputs({
|
|
76
|
+
input: variables,
|
|
77
|
+
artifact: this.artifact,
|
|
78
|
+
})) as _Input
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// trigger the mutation
|
|
82
|
+
const { result } = await executeQuery({
|
|
83
|
+
client: await getCurrentClient(),
|
|
84
|
+
config,
|
|
85
|
+
artifact: this.artifact,
|
|
86
|
+
variables: newVariables,
|
|
87
|
+
session: await getSession(),
|
|
88
|
+
cached: false,
|
|
89
|
+
metadata,
|
|
90
|
+
fetch,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if (result.errors && result.errors.length > 0) {
|
|
94
|
+
this.store.update((s) => ({
|
|
95
|
+
...s,
|
|
96
|
+
errors: result.errors,
|
|
97
|
+
isFetching: false,
|
|
98
|
+
isOptimisticResponse: false,
|
|
99
|
+
data: result.data,
|
|
100
|
+
variables: (newVariables || {}) as _Input,
|
|
101
|
+
}))
|
|
102
|
+
throw result.errors
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// clear the layer holding any mutation results
|
|
106
|
+
layer.clear()
|
|
107
|
+
|
|
108
|
+
// write the result of the mutation to the cache
|
|
109
|
+
cache.write({
|
|
110
|
+
selection: this.artifact.selection,
|
|
111
|
+
data: result.data,
|
|
112
|
+
variables: newVariables,
|
|
113
|
+
// write to the mutation's layer
|
|
114
|
+
layer: layer.id,
|
|
115
|
+
// notify any subscribers that we updated with the optimistic response
|
|
116
|
+
// in order to address situations where the optimistic update was wrong
|
|
117
|
+
notifySubscribers: toNotify,
|
|
118
|
+
// make sure that we notify subscribers for any values that we overwrite
|
|
119
|
+
// in order to address any race conditions when comparing the previous value
|
|
120
|
+
forceNotify: true,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// merge the layer back into the cache
|
|
124
|
+
cache._internal_unstable.storage.resolveLayer(layer.id)
|
|
125
|
+
|
|
126
|
+
// prepare store data
|
|
127
|
+
const storeData: MutationResult<_Data, _Input> = {
|
|
128
|
+
data: unmarshalSelection(config, this.artifact.selection, result.data) as _Data,
|
|
129
|
+
errors: result.errors ?? null,
|
|
130
|
+
isFetching: false,
|
|
131
|
+
isOptimisticResponse: false,
|
|
132
|
+
variables: newVariables,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// update the store value
|
|
136
|
+
this.store.set(storeData)
|
|
137
|
+
|
|
138
|
+
// return the value to the caller
|
|
139
|
+
return storeData.data ?? ({} as _Data)
|
|
140
|
+
} catch (error) {
|
|
141
|
+
this.store.update((s) => ({
|
|
142
|
+
...s,
|
|
143
|
+
errors: error as { message: string }[],
|
|
144
|
+
isFetching: false,
|
|
145
|
+
isOptimisticResponse: false,
|
|
146
|
+
data: null,
|
|
147
|
+
variables: newVariables,
|
|
148
|
+
}))
|
|
149
|
+
|
|
150
|
+
// if the mutation failed, roll the layer back and delete it
|
|
151
|
+
layer.clear()
|
|
152
|
+
cache._internal_unstable.storage.resolveLayer(layer.id)
|
|
153
|
+
|
|
154
|
+
// bubble the mutation error up to the caller
|
|
155
|
+
throw error
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
subscribe(...args: Parameters<Readable<MutationResult<_Data, _Input>>['subscribe']>) {
|
|
160
|
+
// use it's value
|
|
161
|
+
return this.store.subscribe(...args)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private get nullState() {
|
|
165
|
+
return {
|
|
166
|
+
data: null as _Data | null,
|
|
167
|
+
errors: null,
|
|
168
|
+
isFetching: false,
|
|
169
|
+
isOptimisticResponse: false,
|
|
170
|
+
variables: null,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export type MutationConfig<_Result, _Input, _Optimistic> = {
|
|
176
|
+
optimisticResponse?: _Optimistic
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export type MutationResult<_Data, _Input> = {
|
|
180
|
+
data: _Data | null
|
|
181
|
+
errors: { message: string }[] | null
|
|
182
|
+
isFetching: boolean
|
|
183
|
+
isOptimisticResponse: boolean
|
|
184
|
+
variables: _Input | null
|
|
185
|
+
}
|