import type { HoudiniClient } from '$houdini/runtime/client' import { getCurrentConfig } from '$houdini/runtime/config' import type { LoadEvent } from '@sveltejs/kit' import type { HoudiniSvelteConfig } from 'houdini-svelte' import type { FetchContext } from 'houdini/runtime' import * as log from 'houdini/runtime' import type { CachePolicies, GraphQLVariables, GraphQLObject, MutationArtifact, QueryArtifact, QueryResult, } from 'houdini/runtime' import { ArtifactKind, CachePolicy, CompiledQueryKind } from 'houdini/runtime' import { get } from 'svelte/store' import { clientStarted, isBrowser } from '../adapter.js' import { initClient } from '../client.js' import { getSession } from '../session.js' import type { ClientFetchParams, LoadEventFetchParams, QueryStoreFetchParams, RequestEventFetchParams, } from '../types.js' import { BaseStore } from './base.js' export class QueryStore< _Data extends GraphQLObject, _Input extends GraphQLVariables | null | undefined, > extends BaseStore<_Data, _Input, QueryArtifact> { // whether the store requires variables for input variables: boolean // identify it as a query store kind = CompiledQueryKind // if there is a load in progress when the CSF triggers we need to stop it protected loadPending = false // the string identifying the store protected storeName: string constructor({ artifact, storeName, variables }: StoreConfig<_Data, _Input, QueryArtifact>) { super({ artifact, }) this.storeName = storeName this.variables = variables } /** * Fetch the data from the server */ fetch(params?: RequestEventFetchParams<_Data, _Input>): Promise> fetch(params?: LoadEventFetchParams<_Data, _Input>): Promise> fetch(params?: ClientFetchParams<_Data, _Input>): Promise> fetch(params?: QueryStoreFetchParams<_Data, _Input>): Promise> async fetch(args?: QueryStoreFetchParams<_Data, _Input>): Promise> { const client = (await initClient()) as HoudiniClient this.setup(false) // validate and prepare the request context for the current environment (client vs server) // make a shallow copy of the args so we don't mutate the arguments that the user hands us const { policy, params, context } = await fetchParams(this.artifact, this.storeName, args) // identify if this is a CSF or load const isLoadFetch = Boolean('event' in params && params.event) const isComponentFetch = !isLoadFetch // if there is a pending load, don't do anything if (this.loadPending && isComponentFetch) { log.error(`! Encountered fetch from your component while ${this.storeName}.load was running. This will result in duplicate queries. If you are trying to ensure there is always a good value, please use a CachePolicy instead.`) return get(this.observer) } // a component fetch is _always_ blocking if (isComponentFetch) { params.blocking = true } // blocking const config = getCurrentConfig() const config_svelte = (config.plugins as any)['houdini-svelte'] as HoudiniSvelteConfig // Blocking strategy... step by step... Let's respect the order & priority let need_to_block = false // 0/ Check if the config make sense if ( client.throwOnError_operations.includes('all') || client.throwOnError_operations.includes('query') ) { // if explicitly set to not_always_blocking, we can't throw, so warn the user. if (config_svelte.defaultRouteBlocking === false) { log.info( '[Houdini] ! throwOnError with operation "all" or "query", is not compatible with defaultRouteBlocking set to "false"' ) } } // 1/ Check config if (config_svelte.defaultRouteBlocking === true) { need_to_block = true } // 2/ ThrowOnError if ( client.throwOnError_operations.includes('all') || client.throwOnError_operations.includes('query') ) { need_to_block = true } // 4/ params if (params?.blocking === true) { need_to_block = true } else if (params?.blocking === false) { need_to_block = false } // the fetch is happening in a load if (isLoadFetch) { this.loadPending = true } // if the query has a loading state, we never block for the request on the client if (isBrowser && this.artifact.enableLoadingState) { need_to_block = false } // we might not want to actually wait for the fetch to resolve const fakeAwait = clientStarted && isBrowser && !need_to_block // spreading the default variables frist so that if the user provides one of these params themselves, // those params get overwritten with the correct value const usedVariables = { ...this.artifact.input?.defaults, ...params.variables, } // we want to try to load cached data before we potentially fake the await // this makes sure that the UI feels snappy as we click between cached pages // (no loaders) const refersToCache = policy !== CachePolicy.NetworkOnly && policy !== CachePolicy.NoCache if (refersToCache && fakeAwait) { await this.observer.send({ fetch: context.fetch, variables: usedVariables, metadata: params.metadata, session: context.session, policy: CachePolicy.CacheOnly, // if the CacheOnly request doesn't give us anything, // don't update the store silenceEcho: true, }) } // if the query is a live query, we don't really care about network policies any more // since CacheOrNetwork behaves the same as CacheAndNetwork const request = this.observer.send({ fetch: context.fetch, variables: usedVariables, metadata: params.metadata, session: context.session, policy: policy, stuff: {}, }) // if we have to track when the fetch is done, request .then((val) => { this.loadPending = false params.then?.(val.data) }) .catch(() => {}) if (!fakeAwait) { await request } // the store will have been updated already since we waited for the response return get(this.observer) } } // the parameters we will be passed from the generator export type StoreConfig<_Data extends GraphQLObject, _Input, _Artifact> = { artifact: _Artifact storeName: string variables: boolean } export async function fetchParams<_Data extends GraphQLObject, _Input>( artifact: QueryArtifact | MutationArtifact, _storeName: string, params?: QueryStoreFetchParams<_Data, _Input> ): Promise<{ context: FetchContext policy: CachePolicies | undefined params: QueryStoreFetchParams<_Data, _Input> }> { // figure out the right policy let policy = params?.policy if (!policy && artifact.kind === ArtifactKind.Query) { // use the artifact policy as the default, otherwise prefer the cache over the network policy = artifact.policy ?? CachePolicy.CacheOrNetwork } // figure out the right fetch to use let fetchFn: LoadEvent['fetch'] | null = null if (params) { if ('fetch' in params && params.fetch) { fetchFn = params.fetch } else if ('event' in params && params.event && 'fetch' in params.event) { fetchFn = params.event.fetch } } // if we still don't have a fetch function, use the global one (node and browsers both have fetch) if (!fetchFn) { fetchFn = globalThis.fetch.bind(globalThis) } const session = await getSession(params && 'event' in params ? params.event : undefined) return { context: { fetch: fetchFn!, metadata: params?.metadata ?? {}, session, }, policy, params: params ?? {}, } } const _contextError = (storeName: string) => ` ${log.red(`Missing event args in load function`)}. Please remember to pass event to fetch like so: import type { LoadEvent } from '@sveltejs/kit'; // in a load function... export async function load(${log.yellow('event')}: LoadEvent) { return { ...load_${storeName}({ ${log.yellow('event')}, variables: { ... } }) }; } // in a server-side mutation: await mutation.mutate({ ... }, ${log.yellow('{ event }')}) `