From e92e01453f700971d9f2c48032437b3d20c437e0 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 02:35:44 +0100 Subject: [PATCH 01/17] Reimplement @urql/svelte bindings with new API approach --- packages/svelte-urql/src/context.ts | 7 +- packages/svelte-urql/src/index.ts | 2 +- packages/svelte-urql/src/internal.ts | 6 + packages/svelte-urql/src/operationStore.ts | 120 ++++++++++++++++ packages/svelte-urql/src/operations.ts | 136 ++++++++++++++++++ .../svelte-urql/src/operations/constants.ts | 8 -- packages/svelte-urql/src/operations/index.ts | 3 - packages/svelte-urql/src/operations/mutate.ts | 50 ------- packages/svelte-urql/src/operations/query.ts | 129 ----------------- .../src/operations/subscription.ts | 120 ---------------- 10 files changed, 266 insertions(+), 315 deletions(-) create mode 100644 packages/svelte-urql/src/internal.ts create mode 100644 packages/svelte-urql/src/operationStore.ts create mode 100644 packages/svelte-urql/src/operations.ts delete mode 100644 packages/svelte-urql/src/operations/constants.ts delete mode 100644 packages/svelte-urql/src/operations/index.ts delete mode 100644 packages/svelte-urql/src/operations/mutate.ts delete mode 100644 packages/svelte-urql/src/operations/query.ts delete mode 100644 packages/svelte-urql/src/operations/subscription.ts diff --git a/packages/svelte-urql/src/context.ts b/packages/svelte-urql/src/context.ts index 96e8da2eb0..f9443b55eb 100644 --- a/packages/svelte-urql/src/context.ts +++ b/packages/svelte-urql/src/context.ts @@ -1,12 +1,11 @@ import { setContext, getContext } from 'svelte'; import { Client, ClientOptions } from '@urql/core'; +import { _contextKey } from './internal'; -const CLIENT = '$$_URQL'; - -export const getClient = (): Client => getContext(CLIENT); +export const getClient = (): Client => getContext(_contextKey); export const setClient = (client: Client): void => { - setContext(CLIENT, client); + setContext(_contextKey, client); }; export const initClient = (args: ClientOptions): Client => { diff --git a/packages/svelte-urql/src/index.ts b/packages/svelte-urql/src/index.ts index 662f139578..cdecde8406 100644 --- a/packages/svelte-urql/src/index.ts +++ b/packages/svelte-urql/src/index.ts @@ -1,3 +1,3 @@ -export * from '@urql/core'; +export * from './operationStore'; export * from './context'; export * from './operations'; diff --git a/packages/svelte-urql/src/internal.ts b/packages/svelte-urql/src/internal.ts new file mode 100644 index 0000000000..58ab8e1fc5 --- /dev/null +++ b/packages/svelte-urql/src/internal.ts @@ -0,0 +1,6 @@ +export const _contextKey = '$$_urql'; +export const _storeUpdate = new Set(); +export const _markStoreUpdate = + process.env.NODE_ENV !== 'production' + ? (value: object) => _storeUpdate.add(value) + : () => undefined; diff --git a/packages/svelte-urql/src/operationStore.ts b/packages/svelte-urql/src/operationStore.ts new file mode 100644 index 0000000000..e15f3e7e68 --- /dev/null +++ b/packages/svelte-urql/src/operationStore.ts @@ -0,0 +1,120 @@ +import { Writable, writable } from 'svelte/store'; +import { DocumentNode } from 'graphql'; +import { CombinedError } from '@urql/core'; + +import { _storeUpdate } from './internal'; + +type Updater = (value: T) => T; + +/** + * This Svelte store wraps both a `GraphQLRequest` and an `OperationResult`. + * It can be used to update the query and read the subsequent result back. + */ +export interface OperationStore + extends Writable> { + // Input properties + query: DocumentNode | string; + variables: Vars | void | null; + // Output properties + readonly stale: boolean; + readonly fetching: boolean; + readonly data: Data | void; + readonly error?: CombinedError | void; + readonly extensions?: Record | void; +} + +export function operationStore( + query: string | DocumentNode, + variables?: Vars | null +): OperationStore { + const state = { + stale: false, + fetching: true, + data: undefined, + error: undefined, + extensions: undefined, + } as OperationStore; + + const store = writable(state); + const invalidate = store.set.bind(null, state); + + let _internalUpdate = false; + + function set(value: Partial) { + _internalUpdate = true; + if (process.env.NODE_ENV !== 'production') { + _storeUpdate.delete(value); + if (!_storeUpdate.has(value)) { + for (const key in value) { + if (key !== 'query' && key !== 'variables') { + throw new TypeError( + 'It is not allowed to update result properties on an OperationStore .' + ); + } + } + } + } + + for (const key in value) { + if (key === 'query') { + query = value.query!; + } else if (key === 'variables') { + variables = value.variables as Vars; + } else if (key === 'stale' || key === 'fetching') { + (state as any)[key] = !!value[key]; + } else if (key in state) { + state[key] = value[key]; + } + } + + _internalUpdate = false; + invalidate(); + } + + function update(fn: Updater): void { + set(fn(state)); + } + + state.set = set; + state.update = update; + state.subscribe = store.subscribe; + + let result = state; + if (process.env.NODE_ENV !== 'production') { + result = { ...state }; + + ['stale', 'fetching', 'data', 'error', 'extensions'].forEach(prop => { + Object.defineProperty(result, prop, { + configurable: false, + get() { + return state[prop]; + }, + set() { + throw new TypeError( + 'It is not allowed to update result properties on an OperationStore .' + ); + }, + }); + }); + } + + Object.defineProperty(result, 'query', { + configurable: false, + get: () => query, + set(newQuery) { + query = newQuery; + if (!_internalUpdate) invalidate(); + }, + }); + + Object.defineProperty(result, 'variables', { + configurable: false, + get: () => variables, + set(newVariables) { + variables = newVariables; + if (!_internalUpdate) invalidate(); + }, + }); + + return result as OperationStore; +} diff --git a/packages/svelte-urql/src/operations.ts b/packages/svelte-urql/src/operations.ts new file mode 100644 index 0000000000..900fa5026c --- /dev/null +++ b/packages/svelte-urql/src/operations.ts @@ -0,0 +1,136 @@ +import { createRequest, OperationContext, GraphQLRequest } from '@urql/core'; +import { onDestroy } from 'svelte'; + +import { + pipe, + make, + scan, + concat, + fromValue, + switchMap, + subscribe, +} from 'wonka'; + +import { OperationStore, operationStore } from './operationStore'; +import { getClient } from './context'; +import { _markStoreUpdate } from './internal'; + +const baseState: Partial = { + fetching: false, + stale: false, + error: undefined, + data: undefined, + extensions: undefined, +}; + +const isStore = (input: any): input is OperationStore => + input && typeof input.subscribe === 'function'; + +const toStore = (input: GraphQLRequest | OperationStore) => + !isStore(input) ? operationStore(input.query, input.variables) : input; + +const toSource = (store: OperationStore) => { + return make(observer => { + let $request: void | GraphQLRequest; + return store.subscribe(state => { + const request = createRequest(state.query, state.variables as any); + if (!$request || request.key !== $request.key) + observer.next(($request = request)); + }); + }); +}; + +export function query( + input: GraphQLRequest | OperationStore, + context?: Partial +): OperationStore { + const client = getClient(); + const store = toStore(input); + const subscription = pipe( + toSource(store), + switchMap(request => { + return concat>([ + fromValue({ fetching: true, stale: false }), + client.executeQuery(request, context), + fromValue({ fetching: false, stale: false }), + ]); + }), + scan( + (result, partial) => ({ + ...result, + ...partial, + }), + baseState + ), + subscribe(update => { + _markStoreUpdate(update); + store.set(update as OperationStore); + }) + ); + + onDestroy(subscription.unsubscribe); + return store; +} + +export type SubscriptionHandler = (prev: R | undefined, data: T) => R; + +export function subscription( + input: GraphQLRequest | OperationStore, + context?: Partial, + handler?: SubscriptionHandler +): OperationStore { + const client = getClient(); + const store = toStore(input); + const subscription = pipe( + toSource(store), + switchMap(request => { + return concat>([ + fromValue({ fetching: true, stale: false }), + client.executeSubscription(request, context), + fromValue({ fetching: false, stale: false }), + ]); + }), + scan((result, partial: any) => { + const data = + partial.data !== undefined + ? typeof handler === 'function' + ? handler(result.data, partial.data) + : partial.data + : result.data; + return { ...result, ...partial, data }; + }, baseState), + subscribe(update => { + _markStoreUpdate(update); + store.set(update as OperationStore); + }) + ); + + onDestroy(subscription.unsubscribe); + return store; +} + +export type ExecuteMutation = ( + variables?: V, + context?: Partial +) => Promise>; + +export function mutation( + input: GraphQLRequest | OperationStore +): ExecuteMutation { + const client = getClient(); + const store = toStore(input); + + return (vars, context) => { + if (vars) store.variables = vars; + return new Promise(resolve => { + client + .mutation(store.query, store.variables, context) + .toPromise() + .then(update => { + _markStoreUpdate(update); + store.set(update as any); + resolve(store); + }); + }); + }; +} diff --git a/packages/svelte-urql/src/operations/constants.ts b/packages/svelte-urql/src/operations/constants.ts deleted file mode 100644 index 2a3c57f4c2..0000000000 --- a/packages/svelte-urql/src/operations/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const initialState = { - fetching: false, - stale: false, - error: undefined, - data: undefined, - operation: undefined, - extensions: undefined, -}; diff --git a/packages/svelte-urql/src/operations/index.ts b/packages/svelte-urql/src/operations/index.ts deleted file mode 100644 index 48447ffdee..0000000000 --- a/packages/svelte-urql/src/operations/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './mutate'; -export * from './query'; -export * from './subscription'; diff --git a/packages/svelte-urql/src/operations/mutate.ts b/packages/svelte-urql/src/operations/mutate.ts deleted file mode 100644 index 7f2686e710..0000000000 --- a/packages/svelte-urql/src/operations/mutate.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { pipe, subscribe } from 'wonka'; -import { OperationResult, OperationContext } from '@urql/core'; -import { Readable } from 'svelte/store'; -import { DocumentNode } from 'graphql'; - -import { getClient } from '../context'; - -export interface MutationArguments { - query: string | DocumentNode; - variables?: V; - context?: Partial; -} - -export interface MutationStore - extends Readable>, - PromiseLike> { - (additionalArgs?: Partial>): Promise>; -} - -export const mutate = ( - args: MutationArguments -): MutationStore => { - const client = getClient(); - - function mutate$(additionalArgs?: Partial>) { - const mergedArgs = { ...args, ...additionalArgs }; - return client - .mutation( - mergedArgs.query, - mergedArgs.variables as any, - mergedArgs.context - ) - .toPromise(); - } - - mutate$.subscribe = (onValue: (result: OperationResult) => void) => { - return pipe( - client.mutation(args.query, args.variables as any, args.context), - subscribe(onValue) - ).unsubscribe; - }; - - mutate$.then = ( - onValue: (result: OperationResult) => any - ): Promise => { - return mutate$().then(onValue); - }; - - return mutate$ as any; -}; diff --git a/packages/svelte-urql/src/operations/query.ts b/packages/svelte-urql/src/operations/query.ts deleted file mode 100644 index 47a01ddae1..0000000000 --- a/packages/svelte-urql/src/operations/query.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { - pipe, - makeSubject, - fromValue, - switchMap, - onStart, - concat, - scan, - map, - take, - share, - subscribe, - publish, - toPromise, -} from 'wonka'; - -import { - RequestPolicy, - OperationContext, - CombinedError, - Operation, -} from '@urql/core'; - -import { Readable } from 'svelte/store'; -import { DocumentNode } from 'graphql'; - -import { getClient } from '../context'; -import { initialState } from './constants'; - -export interface QueryArguments { - query: string | DocumentNode; - variables?: V; - requestPolicy?: RequestPolicy; - pollInterval?: number; - pause?: boolean; - context?: Partial; -} - -export interface QueryResult { - fetching: boolean; - stale: boolean; - data?: T; - error?: CombinedError; - extensions?: Record; - operation?: Operation; -} - -export interface QueryStore - extends Readable>, - PromiseLike> { - (args?: Partial>): QueryStore; -} - -export const query = ( - args: QueryArguments -): QueryStore => { - const client = getClient(); - const { source: args$, next: nextArgs } = makeSubject>(); - - const queryResult$ = pipe( - args$, - switchMap(args => { - if (args.pause) { - return fromValue({ fetching: false, stale: false }); - } - - return concat([ - // Initially set fetching to true - fromValue({ fetching: true, stale: false }), - pipe( - client.query(args.query, args.variables, { - requestPolicy: args.requestPolicy, - pollInterval: args.pollInterval, - ...args.context, - }), - map(({ stale, data, error, extensions, operation }) => ({ - fetching: false, - stale: !!stale, - data, - error, - extensions, - operation, - })) - ), - // When the source proactively closes, fetching is set to false - fromValue({ fetching: false, stale: false }), - ]); - }), - // The individual partial results are merged into each previous result - scan( - (result, partial) => ({ - ...result, - ...partial, - }), - initialState - ), - share - ); - - publish(queryResult$); - - const queryStore = (baseArgs: QueryArguments): QueryStore => { - const result$ = pipe( - queryResult$, - onStart(() => { - nextArgs({ ...baseArgs, ...args }); - }) - ); - - function query$(args?: Partial>) { - return queryStore({ - ...baseArgs, - ...args, - }); - } - - query$.subscribe = (onValue: (result: QueryResult) => void) => { - return pipe(result$, subscribe(onValue)).unsubscribe; - }; - - query$.then = (onValue: (result: QueryResult) => any): Promise => { - return pipe(result$, take(1), toPromise).then(onValue); - }; - - return query$ as any; - }; - - return queryStore(args); -}; diff --git a/packages/svelte-urql/src/operations/subscription.ts b/packages/svelte-urql/src/operations/subscription.ts deleted file mode 100644 index 972e2ea810..0000000000 --- a/packages/svelte-urql/src/operations/subscription.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { - pipe, - makeSubject, - fromValue, - switchMap, - onStart, - concat, - scan, - map, - share, - subscribe, - publish, -} from 'wonka'; - -import { OperationContext, CombinedError, Operation } from '@urql/core'; -import { Readable } from 'svelte/store'; -import { DocumentNode } from 'graphql'; - -import { getClient } from '../context'; -import { initialState } from './constants'; - -export interface SubscriptionArguments { - query: string | DocumentNode; - variables?: V; - pause?: boolean; - context?: Partial; - operation?: Operation; -} - -export type SubscriptionHandler = (prev: R | undefined, data: T) => R; - -export interface SubscriptionResult { - fetching: boolean; - stale: boolean; - data?: T; - error?: CombinedError; - extensions?: Record; -} - -export interface SubscriptionStore - extends Readable> { - (args?: Partial>): SubscriptionStore; -} - -export const subscription = ( - args: SubscriptionArguments, - handler?: SubscriptionHandler -): SubscriptionStore => { - const client = getClient(); - const { source: args$, next: nextArgs } = makeSubject< - SubscriptionArguments - >(); - - const subscriptionResult$ = pipe( - args$, - switchMap(args => { - if (args.pause) { - return fromValue({ fetching: false, stale: false }); - } - - return concat([ - // Initially set fetching to true - fromValue({ fetching: true, stale: false }), - pipe( - client.subscription(args.query, args.variables, args.context), - map(({ stale, data, error, extensions, operation }) => ({ - fetching: false, - stale: !!stale, - data, - error, - extensions, - operation, - })) - ), - // When the source proactively closes, fetching is set to false - fromValue({ fetching: false, stale: false }), - ]); - }), - // The individual partial results are merged into each previous result - scan((result, partial: any) => { - const data = - partial.data !== undefined - ? typeof handler === 'function' - ? handler(result.data, partial.data) - : partial.data - : result.data; - return { ...result, ...partial, data }; - }, initialState), - share - ); - - publish(subscriptionResult$); - - const subscriptionStore = ( - baseArgs: SubscriptionArguments - ): SubscriptionStore => { - function subscription$(args?: Partial>) { - return subscriptionStore({ - ...baseArgs, - ...args, - }); - } - - subscription$.subscribe = ( - onValue: (result: SubscriptionResult) => void - ) => { - return pipe( - subscriptionResult$, - onStart(() => { - nextArgs({ ...baseArgs, ...args }); - }), - subscribe(onValue) - ).unsubscribe; - }; - - return subscription$ as any; - }; - - return subscriptionStore(args); -}; From b6813350c24d23c6fbb2c6cd5fb0d2fac1c58ded Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 02:43:08 +0100 Subject: [PATCH 02/17] Accept only stores as input in query/subscription When GraphQLRequest is passed this is usually fine, but when the operation is then instantiated multiple times per component lifecycle, this causes a memory leak of multiple active subscriptions until the component unmounts. This can simply be discouraged by requiring the store. --- packages/svelte-urql/src/operations.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/svelte-urql/src/operations.ts b/packages/svelte-urql/src/operations.ts index 900fa5026c..1c3e32c4c1 100644 --- a/packages/svelte-urql/src/operations.ts +++ b/packages/svelte-urql/src/operations.ts @@ -23,12 +23,6 @@ const baseState: Partial = { extensions: undefined, }; -const isStore = (input: any): input is OperationStore => - input && typeof input.subscribe === 'function'; - -const toStore = (input: GraphQLRequest | OperationStore) => - !isStore(input) ? operationStore(input.query, input.variables) : input; - const toSource = (store: OperationStore) => { return make(observer => { let $request: void | GraphQLRequest; @@ -41,11 +35,10 @@ const toSource = (store: OperationStore) => { }; export function query( - input: GraphQLRequest | OperationStore, + store: OperationStore, context?: Partial ): OperationStore { const client = getClient(); - const store = toStore(input); const subscription = pipe( toSource(store), switchMap(request => { @@ -75,12 +68,11 @@ export function query( export type SubscriptionHandler = (prev: R | undefined, data: T) => R; export function subscription( - input: GraphQLRequest | OperationStore, + store: OperationStore, context?: Partial, handler?: SubscriptionHandler ): OperationStore { const client = getClient(); - const store = toStore(input); const subscription = pipe( toSource(store), switchMap(request => { @@ -118,7 +110,11 @@ export function mutation( input: GraphQLRequest | OperationStore ): ExecuteMutation { const client = getClient(); - const store = toStore(input); + + const store = + typeof (input as any).subscribe !== 'function' + ? operationStore(input.query, input.variables) + : (input as OperationStore); return (vars, context) => { if (vars) store.variables = vars; From bf8ac073347f1ecc177debbd9b93c7367f8c1008 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 02:46:45 +0100 Subject: [PATCH 03/17] Readd @urql/core re-export --- packages/svelte-urql/package.json | 2 +- packages/svelte-urql/src/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte-urql/package.json b/packages/svelte-urql/package.json index 828ecf9d25..56ecde0491 100644 --- a/packages/svelte-urql/package.json +++ b/packages/svelte-urql/package.json @@ -55,7 +55,7 @@ "svelte": "^3.0.0" }, "dependencies": { - "@urql/core": "^1.12.3", + "@urql/core": "^1.13.1", "wonka": "^4.0.14" }, "devDependencies": { diff --git a/packages/svelte-urql/src/index.ts b/packages/svelte-urql/src/index.ts index cdecde8406..f8acafa614 100644 --- a/packages/svelte-urql/src/index.ts +++ b/packages/svelte-urql/src/index.ts @@ -1,3 +1,4 @@ +export * from '@urql/core'; export * from './operationStore'; export * from './context'; export * from './operations'; From 7bb860f3badf3468233f2bd729a3e11d1c3a2127 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 02:56:02 +0100 Subject: [PATCH 04/17] Fix set in development and allow empty updates --- packages/svelte-urql/src/operationStore.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/svelte-urql/src/operationStore.ts b/packages/svelte-urql/src/operationStore.ts index e15f3e7e68..d16cff7e23 100644 --- a/packages/svelte-urql/src/operationStore.ts +++ b/packages/svelte-urql/src/operationStore.ts @@ -4,6 +4,8 @@ import { CombinedError } from '@urql/core'; import { _storeUpdate } from './internal'; +const noop = Object.create(null); + type Updater = (value: T) => T; /** @@ -40,11 +42,12 @@ export function operationStore( let _internalUpdate = false; - function set(value: Partial) { + function set(value?: Partial) { + if (!value) value = noop; + _internalUpdate = true; if (process.env.NODE_ENV !== 'production') { - _storeUpdate.delete(value); - if (!_storeUpdate.has(value)) { + if (!_storeUpdate.has(value!)) { for (const key in value) { if (key !== 'query' && key !== 'variables') { throw new TypeError( @@ -53,6 +56,8 @@ export function operationStore( } } } + + _storeUpdate.delete(value!); } for (const key in value) { From fc7f99f97554c0026a330fca228d95c67e842842 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 03:01:41 +0100 Subject: [PATCH 05/17] Move context options to operationStore --- packages/svelte-urql/src/operationStore.ts | 19 ++++++++++++++++--- packages/svelte-urql/src/operations.ts | 8 +++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/svelte-urql/src/operationStore.ts b/packages/svelte-urql/src/operationStore.ts index d16cff7e23..680b914dd9 100644 --- a/packages/svelte-urql/src/operationStore.ts +++ b/packages/svelte-urql/src/operationStore.ts @@ -1,6 +1,6 @@ import { Writable, writable } from 'svelte/store'; import { DocumentNode } from 'graphql'; -import { CombinedError } from '@urql/core'; +import { OperationContext, CombinedError } from '@urql/core'; import { _storeUpdate } from './internal'; @@ -16,7 +16,8 @@ export interface OperationStore extends Writable> { // Input properties query: DocumentNode | string; - variables: Vars | void | null; + variables: Vars | undefined | null; + context: Partial | undefined; // Output properties readonly stale: boolean; readonly fetching: boolean; @@ -27,7 +28,8 @@ export interface OperationStore export function operationStore( query: string | DocumentNode, - variables?: Vars | null + variables?: Vars | null, + context?: Partial ): OperationStore { const state = { stale: false, @@ -65,6 +67,8 @@ export function operationStore( query = value.query!; } else if (key === 'variables') { variables = value.variables as Vars; + } else if (key === 'context') { + context = value.context as Partial; } else if (key === 'stale' || key === 'fetching') { (state as any)[key] = !!value[key]; } else if (key in state) { @@ -121,5 +125,14 @@ export function operationStore( }, }); + Object.defineProperty(result, 'context', { + configurable: false, + get: () => context, + set(newContext) { + context = newContext; + if (!_internalUpdate) invalidate(); + }, + }); + return result as OperationStore; } diff --git a/packages/svelte-urql/src/operations.ts b/packages/svelte-urql/src/operations.ts index 1c3e32c4c1..d77e9e4e9e 100644 --- a/packages/svelte-urql/src/operations.ts +++ b/packages/svelte-urql/src/operations.ts @@ -35,8 +35,7 @@ const toSource = (store: OperationStore) => { }; export function query( - store: OperationStore, - context?: Partial + store: OperationStore ): OperationStore { const client = getClient(); const subscription = pipe( @@ -44,7 +43,7 @@ export function query( switchMap(request => { return concat>([ fromValue({ fetching: true, stale: false }), - client.executeQuery(request, context), + client.executeQuery(request, store.context!), fromValue({ fetching: false, stale: false }), ]); }), @@ -69,7 +68,6 @@ export type SubscriptionHandler = (prev: R | undefined, data: T) => R; export function subscription( store: OperationStore, - context?: Partial, handler?: SubscriptionHandler ): OperationStore { const client = getClient(); @@ -78,7 +76,7 @@ export function subscription( switchMap(request => { return concat>([ fromValue({ fetching: true, stale: false }), - client.executeSubscription(request, context), + client.executeSubscription(request, store.context), fromValue({ fetching: false, stale: false }), ]); }), From cec8a61aa2a691751694f50ca51e424ab492a45b Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 03:10:43 +0100 Subject: [PATCH 06/17] Update operationStore to simplify defineProperty calls --- packages/svelte-urql/src/operationStore.ts | 54 ++++++++-------------- 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/packages/svelte-urql/src/operationStore.ts b/packages/svelte-urql/src/operationStore.ts index 680b914dd9..1c0e62bd71 100644 --- a/packages/svelte-urql/src/operationStore.ts +++ b/packages/svelte-urql/src/operationStore.ts @@ -31,6 +31,12 @@ export function operationStore( variables?: Vars | null, context?: Partial ): OperationStore { + const internal = { + query, + variables, + context, + }; + const state = { stale: false, fetching: true, @@ -63,12 +69,8 @@ export function operationStore( } for (const key in value) { - if (key === 'query') { - query = value.query!; - } else if (key === 'variables') { - variables = value.variables as Vars; - } else if (key === 'context') { - context = value.context as Partial; + if (key === 'query' || key === 'variables' || key === 'context') { + (internal as any)[key] = value[key]; } else if (key === 'stale' || key === 'fetching') { (state as any)[key] = !!value[key]; } else if (key in state) { @@ -92,7 +94,7 @@ export function operationStore( if (process.env.NODE_ENV !== 'production') { result = { ...state }; - ['stale', 'fetching', 'data', 'error', 'extensions'].forEach(prop => { + for (const prop in state) { Object.defineProperty(result, prop, { configurable: false, get() { @@ -104,35 +106,19 @@ export function operationStore( ); }, }); - }); + } } - Object.defineProperty(result, 'query', { - configurable: false, - get: () => query, - set(newQuery) { - query = newQuery; - if (!_internalUpdate) invalidate(); - }, - }); - - Object.defineProperty(result, 'variables', { - configurable: false, - get: () => variables, - set(newVariables) { - variables = newVariables; - if (!_internalUpdate) invalidate(); - }, - }); - - Object.defineProperty(result, 'context', { - configurable: false, - get: () => context, - set(newContext) { - context = newContext; - if (!_internalUpdate) invalidate(); - }, - }); + for (const prop in internal) { + Object.defineProperty(result, 'query', { + configurable: false, + get: () => internal[prop], + set(value) { + internal[prop] = value; + if (!_internalUpdate) invalidate(); + }, + }); + } return result as OperationStore; } From 82baecb29f8d8d0589aa253fde960df93886a4f1 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 12:14:00 +0100 Subject: [PATCH 07/17] Add tests for operationStore --- .../svelte-urql/src/operationStore.test.ts | 77 +++++++++++++++++++ packages/svelte-urql/src/operationStore.ts | 13 ++-- 2 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 packages/svelte-urql/src/operationStore.test.ts diff --git a/packages/svelte-urql/src/operationStore.test.ts b/packages/svelte-urql/src/operationStore.test.ts new file mode 100644 index 0000000000..fddb74aca3 --- /dev/null +++ b/packages/svelte-urql/src/operationStore.test.ts @@ -0,0 +1,77 @@ +import { operationStore } from './operationStore'; +import { _markStoreUpdate } from './internal'; + +it('instantiates an operation container acting as a store', () => { + const variables = {}; + const context = {}; + const store = operationStore('{ test }', variables, context); + + expect(store.query).toBe('{ test }'); + expect(store.variables).toBe(variables); + expect(store.context).toBe(context); + + const subscriber = jest.fn(); + store.subscribe(subscriber); + + expect(subscriber).toHaveBeenCalledWith(store); + expect(subscriber).toHaveBeenCalledTimes(1); + + store.set({ query: '{ test2 }' }); + expect(subscriber).toHaveBeenCalledWith(store); + expect(subscriber).toHaveBeenCalledTimes(2); + + expect(store.query).toBe('{ test2 }'); +}); + +it('adds getters and setters for known values', () => { + const variables = {}; + const context = {}; + const store = operationStore('{ test }', variables, context); + + const update = { + query: '{ update }', + variables: undefined, + context: { requestPolicy: 'cache-and-network' }, + stale: true, + fetching: true, + data: { update: true }, + error: undefined, + extensions: undefined, + }; + + _markStoreUpdate(update); + store.set(update as any); + + expect(store.query).toBe(update.query); + expect(store.variables).toBe(update.variables); + expect(store.context).toBe(update.context); + expect(store.stale).toBe(update.stale); + expect(store.fetching).toBe(update.fetching); + expect(store.data).toBe(update.data); + expect(store.error).toBe(update.error); + expect(store.extensions).toBe(update.extensions); + + const subscriber = jest.fn(); + store.subscribe(subscriber); + expect(subscriber).toHaveBeenCalledTimes(1); + + store.query = '{ imperative }'; + expect(subscriber).toHaveBeenCalledTimes(2); + expect(store.query).toBe('{ imperative }'); +}); + +it('throws when illegal values are set', () => { + const store = operationStore('{ test }'); + + expect(() => { + (store as any).variables = {}; + }).not.toThrow(); + + expect(() => { + (store as any).data = {}; + }).toThrow(); + + expect(() => { + (store as any).set({ error: null }); + }).toThrow(); +}); diff --git a/packages/svelte-urql/src/operationStore.ts b/packages/svelte-urql/src/operationStore.ts index 1c0e62bd71..d9ff9285bb 100644 --- a/packages/svelte-urql/src/operationStore.ts +++ b/packages/svelte-urql/src/operationStore.ts @@ -1,4 +1,4 @@ -import { Writable, writable } from 'svelte/store'; +import { Readable, writable } from 'svelte/store'; import { DocumentNode } from 'graphql'; import { OperationContext, CombinedError } from '@urql/core'; @@ -13,7 +13,7 @@ type Updater = (value: T) => T; * It can be used to update the query and read the subsequent result back. */ export interface OperationStore - extends Writable> { + extends Readable> { // Input properties query: DocumentNode | string; variables: Vars | undefined | null; @@ -24,6 +24,9 @@ export interface OperationStore readonly data: Data | void; readonly error?: CombinedError | void; readonly extensions?: Record | void; + // Writable properties + set(value: Partial>): void; + update(updater: Updater>>): void; } export function operationStore( @@ -59,7 +62,7 @@ export function operationStore( for (const key in value) { if (key !== 'query' && key !== 'variables') { throw new TypeError( - 'It is not allowed to update result properties on an OperationStore .' + 'It is not allowed to update result properties on an OperationStore.' ); } } @@ -102,7 +105,7 @@ export function operationStore( }, set() { throw new TypeError( - 'It is not allowed to update result properties on an OperationStore .' + 'It is not allowed to update result properties on an OperationStore.' ); }, }); @@ -110,7 +113,7 @@ export function operationStore( } for (const prop in internal) { - Object.defineProperty(result, 'query', { + Object.defineProperty(result, prop, { configurable: false, get: () => internal[prop], set(value) { From 5d489ef2895c355831c894bab5a1d12b207b1f15 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 13:08:41 +0100 Subject: [PATCH 08/17] Add tests for query and subscription operations --- .../svelte-urql/src/operationStore.test.ts | 4 + packages/svelte-urql/src/operationStore.ts | 38 ++-- packages/svelte-urql/src/operations.test.ts | 197 ++++++++++++++++++ packages/svelte-urql/src/operations.ts | 6 +- 4 files changed, 231 insertions(+), 14 deletions(-) create mode 100644 packages/svelte-urql/src/operations.test.ts diff --git a/packages/svelte-urql/src/operationStore.test.ts b/packages/svelte-urql/src/operationStore.test.ts index fddb74aca3..8c263cbad1 100644 --- a/packages/svelte-urql/src/operationStore.test.ts +++ b/packages/svelte-urql/src/operationStore.test.ts @@ -55,6 +55,10 @@ it('adds getters and setters for known values', () => { store.subscribe(subscriber); expect(subscriber).toHaveBeenCalledTimes(1); + const state = subscriber.mock.calls[0][0]; + expect(state.stale).toBe(true); + expect(state.query).toBe('{ update }'); + store.query = '{ imperative }'; expect(subscriber).toHaveBeenCalledTimes(2); expect(store.query).toBe('{ imperative }'); diff --git a/packages/svelte-urql/src/operationStore.ts b/packages/svelte-urql/src/operationStore.ts index d9ff9285bb..ede59efaf1 100644 --- a/packages/svelte-urql/src/operationStore.ts +++ b/packages/svelte-urql/src/operationStore.ts @@ -93,9 +93,19 @@ export function operationStore( state.update = update; state.subscribe = store.subscribe; - let result = state; + for (const prop in internal) { + Object.defineProperty(state, prop, { + configurable: false, + get: () => internal[prop], + set(value) { + internal[prop] = value; + if (!_internalUpdate) invalidate(); + }, + }); + } + if (process.env.NODE_ENV !== 'production') { - result = { ...state }; + const result = { ...state }; for (const prop in state) { Object.defineProperty(result, prop, { @@ -110,18 +120,20 @@ export function operationStore( }, }); } - } - for (const prop in internal) { - Object.defineProperty(result, prop, { - configurable: false, - get: () => internal[prop], - set(value) { - internal[prop] = value; - if (!_internalUpdate) invalidate(); - }, - }); + for (const prop in internal) { + Object.defineProperty(result, prop, { + configurable: false, + get: () => internal[prop], + set(value) { + internal[prop] = value; + if (!_internalUpdate) invalidate(); + }, + }); + } + + return result; } - return result as OperationStore; + return state; } diff --git a/packages/svelte-urql/src/operations.test.ts b/packages/svelte-urql/src/operations.test.ts new file mode 100644 index 0000000000..02085bab04 --- /dev/null +++ b/packages/svelte-urql/src/operations.test.ts @@ -0,0 +1,197 @@ +import { makeSubject } from 'wonka'; +import { createClient } from '@urql/core'; +import { operationStore } from './operationStore'; +import { query, subscription } from './operations'; + +const client = createClient({ url: '/graphql' }); + +jest.mock('./context', () => ({ getClient: () => client })); +jest.mock('svelte', () => ({ onDestroy: () => undefined })); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('query', () => { + it('susbcribes to a query and updates data', () => { + const subscriber = jest.fn(); + const subject = makeSubject(); + const executeQuery = jest + .spyOn(client, 'executeQuery') + .mockImplementation(() => subject.source); + + const store = operationStore('{ test }'); + store.subscribe(subscriber); + + query(store); + + expect(executeQuery).toHaveBeenCalledWith( + { + key: expect.any(Number), + query: expect.any(Object), + variables: {}, + }, + undefined + ); + + expect(subscriber).toHaveBeenCalledTimes(2); + expect(store.fetching).toBe(true); + + subject.next({ data: { test: true } }); + expect(subscriber).toHaveBeenCalledTimes(3); + expect(store.data).toEqual({ test: true }); + + subject.complete(); + expect(subscriber).toHaveBeenCalledTimes(4); + expect(store.fetching).toBe(false); + }); + + it('updates the executed query when inputs change', () => { + const subscriber = jest.fn(); + const subject = makeSubject(); + const executeQuery = jest + .spyOn(client, 'executeQuery') + .mockImplementation(() => subject.source); + + const store = operationStore('{ test }'); + store.subscribe(subscriber); + + query(store); + + expect(executeQuery).toHaveBeenCalledWith( + { + key: expect.any(Number), + query: expect.any(Object), + variables: {}, + }, + undefined + ); + + subject.next({ data: { test: true } }); + expect(subscriber).toHaveBeenCalledTimes(3); + expect(store.data).toEqual({ test: true }); + + store.variables = { test: true }; + expect(executeQuery).toHaveBeenCalledTimes(2); + expect(executeQuery).toHaveBeenCalledWith( + { + key: expect.any(Number), + query: expect.any(Object), + variables: { test: true }, + }, + undefined + ); + + expect(subscriber).toHaveBeenCalledTimes(5); + expect(store.fetching).toBe(true); + expect(store.data).toEqual({ test: true }); + }); +}); + +describe('subscription', () => { + it('susbcribes to a subscription and updates data', () => { + const subscriber = jest.fn(); + const subject = makeSubject(); + const executeQuery = jest + .spyOn(client, 'executeSubscription') + .mockImplementation(() => subject.source); + + const store = operationStore('subscription { test }'); + store.subscribe(subscriber); + + subscription(store); + + expect(executeQuery).toHaveBeenCalledWith( + { + key: expect.any(Number), + query: expect.any(Object), + variables: {}, + }, + undefined + ); + + expect(subscriber).toHaveBeenCalledTimes(2); + expect(store.fetching).toBe(true); + + subject.next({ data: { test: true } }); + expect(subscriber).toHaveBeenCalledTimes(3); + expect(store.data).toEqual({ test: true }); + + subject.complete(); + expect(subscriber).toHaveBeenCalledTimes(4); + expect(store.fetching).toBe(false); + }); + + it('updates the executed subscription when inputs change', () => { + const subscriber = jest.fn(); + const subject = makeSubject(); + const executeSubscription = jest + .spyOn(client, 'executeSubscription') + .mockImplementation(() => subject.source); + + const store = operationStore('{ test }'); + store.subscribe(subscriber); + + subscription(store); + + expect(executeSubscription).toHaveBeenCalledWith( + { + key: expect.any(Number), + query: expect.any(Object), + variables: {}, + }, + undefined + ); + + subject.next({ data: { test: true } }); + expect(subscriber).toHaveBeenCalledTimes(3); + expect(store.data).toEqual({ test: true }); + + store.variables = { test: true }; + expect(executeSubscription).toHaveBeenCalledTimes(2); + expect(executeSubscription).toHaveBeenCalledWith( + { + key: expect.any(Number), + query: expect.any(Object), + variables: { test: true }, + }, + undefined + ); + + expect(subscriber).toHaveBeenCalledTimes(5); + expect(store.fetching).toBe(true); + expect(store.data).toEqual({ test: true }); + }); + + it('supports a custom scanning handler', () => { + const subscriber = jest.fn(); + const subject = makeSubject(); + const executeSubscription = jest + .spyOn(client, 'executeSubscription') + .mockImplementation(() => subject.source); + + const store = operationStore('{ counter }'); + store.subscribe(subscriber); + + subscription(store, (prev, current) => ({ + counter: (prev ? prev.counter : 0) + current.counter, + })); + + expect(executeSubscription).toHaveBeenCalledWith( + { + key: expect.any(Number), + query: expect.any(Object), + variables: {}, + }, + undefined + ); + + subject.next({ data: { counter: 1 } }); + expect(subscriber).toHaveBeenCalledTimes(3); + expect(store.data).toEqual({ counter: 1 }); + + subject.next({ data: { counter: 2 } }); + expect(subscriber).toHaveBeenCalledTimes(4); + expect(store.data).toEqual({ counter: 3 }); + }); +}); diff --git a/packages/svelte-urql/src/operations.ts b/packages/svelte-urql/src/operations.ts index d77e9e4e9e..0a4a14fbf0 100644 --- a/packages/svelte-urql/src/operations.ts +++ b/packages/svelte-urql/src/operations.ts @@ -3,6 +3,7 @@ import { onDestroy } from 'svelte'; import { pipe, + map, make, scan, concat, @@ -43,7 +44,10 @@ export function query( switchMap(request => { return concat>([ fromValue({ fetching: true, stale: false }), - client.executeQuery(request, store.context!), + pipe( + client.executeQuery(request, store.context!), + map(result => ({ fetching: false, ...result })) + ), fromValue({ fetching: false, stale: false }), ]); }), From 8b92d2f400ca202759abf6e9962e0cb629a33588 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 13:22:37 +0100 Subject: [PATCH 09/17] Add tests for mutation operation --- packages/svelte-urql/src/operations.test.ts | 43 ++++++++++++++++++--- packages/svelte-urql/src/operations.ts | 10 +++-- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/packages/svelte-urql/src/operations.test.ts b/packages/svelte-urql/src/operations.test.ts index 02085bab04..707c9d4838 100644 --- a/packages/svelte-urql/src/operations.test.ts +++ b/packages/svelte-urql/src/operations.test.ts @@ -1,7 +1,7 @@ -import { makeSubject } from 'wonka'; +import { makeSubject, pipe, take, toPromise } from 'wonka'; import { createClient } from '@urql/core'; import { operationStore } from './operationStore'; -import { query, subscription } from './operations'; +import { query, subscription, mutation } from './operations'; const client = createClient({ url: '/graphql' }); @@ -13,7 +13,7 @@ beforeEach(() => { }); describe('query', () => { - it('susbcribes to a query and updates data', () => { + it('subscribes to a query and updates data', () => { const subscriber = jest.fn(); const subject = makeSubject(); const executeQuery = jest @@ -89,7 +89,7 @@ describe('query', () => { }); describe('subscription', () => { - it('susbcribes to a subscription and updates data', () => { + it('subscribes to a subscription and updates data', () => { const subscriber = jest.fn(); const subject = makeSubject(); const executeQuery = jest @@ -170,7 +170,7 @@ describe('subscription', () => { .spyOn(client, 'executeSubscription') .mockImplementation(() => subject.source); - const store = operationStore('{ counter }'); + const store = operationStore('subscription { counter }'); store.subscribe(subscriber); subscription(store, (prev, current) => ({ @@ -195,3 +195,36 @@ describe('subscription', () => { expect(store.data).toEqual({ counter: 3 }); }); }); + +describe('mutation', () => { + it('provides an execute method that resolves a promise', async () => { + const subscriber = jest.fn(); + const subject = makeSubject(); + const clientMutation = jest + .spyOn(client, 'mutation') + .mockImplementation((): any => ({ + toPromise() { + return pipe(subject.source, take(1), toPromise); + }, + })); + + const store = operationStore('mutation { test }', { test: false }); + store.subscribe(subscriber); + + const start = mutation(store); + expect(subscriber).toHaveBeenCalledTimes(1); + expect(clientMutation).not.toHaveBeenCalled(); + + const result$ = start({ test: true }); + expect(subscriber).toHaveBeenCalledTimes(2); + expect(store.fetching).toBe(true); + expect(store.variables).toEqual({ test: true }); + + subject.next({ data: { test: true } }); + expect(await result$).toEqual(store); + + expect(subscriber).toHaveBeenCalledTimes(3); + expect(store.fetching).toBe(false); + expect(store.data).toEqual({ test: true }); + }); +}); diff --git a/packages/svelte-urql/src/operations.ts b/packages/svelte-urql/src/operations.ts index 0a4a14fbf0..b0e306ce5d 100644 --- a/packages/svelte-urql/src/operations.ts +++ b/packages/svelte-urql/src/operations.ts @@ -119,14 +119,18 @@ export function mutation( : (input as OperationStore); return (vars, context) => { - if (vars) store.variables = vars; + const update = { fetching: true, variables: vars || store.variables }; + _markStoreUpdate(update); + store.set(update); + return new Promise(resolve => { client .mutation(store.query, store.variables, context) .toPromise() - .then(update => { + .then(result => { + const update = { fetching: false, ...result }; _markStoreUpdate(update); - store.set(update as any); + store.set(update); resolve(store); }); }); From 783598c4fdde178b2a8ad0f032763f1a21c56913 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 13:28:05 +0100 Subject: [PATCH 10/17] Add forced update due to changed context --- packages/svelte-urql/src/operations.test.ts | 13 +++++++++++++ packages/svelte-urql/src/operations.ts | 9 ++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/svelte-urql/src/operations.test.ts b/packages/svelte-urql/src/operations.test.ts index 707c9d4838..5cbd36346e 100644 --- a/packages/svelte-urql/src/operations.test.ts +++ b/packages/svelte-urql/src/operations.test.ts @@ -85,6 +85,19 @@ describe('query', () => { expect(subscriber).toHaveBeenCalledTimes(5); expect(store.fetching).toBe(true); expect(store.data).toEqual({ test: true }); + + store.context = { requestPolicy: 'cache-and-network' }; + expect(executeQuery).toHaveBeenCalledTimes(3); + expect(executeQuery).toHaveBeenCalledWith( + { + key: expect.any(Number), + query: expect.any(Object), + variables: { test: true }, + }, + { + requestPolicy: 'cache-and-network', + } + ); }); }); diff --git a/packages/svelte-urql/src/operations.ts b/packages/svelte-urql/src/operations.ts index b0e306ce5d..c2bf500a22 100644 --- a/packages/svelte-urql/src/operations.ts +++ b/packages/svelte-urql/src/operations.ts @@ -27,10 +27,17 @@ const baseState: Partial = { const toSource = (store: OperationStore) => { return make(observer => { let $request: void | GraphQLRequest; + let $context: void | Partial; return store.subscribe(state => { const request = createRequest(state.query, state.variables as any); - if (!$request || request.key !== $request.key) + if ( + $context !== state.context || + !$request || + request.key !== $request.key + ) { + $context = state.context; observer.next(($request = request)); + } }); }); }; From 3acebc6ad9aa06c09482b3fb077f7be54a7b27ad Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 14:15:53 +0100 Subject: [PATCH 11/17] Simplify invalidate logic in operationStore --- packages/svelte-urql/src/operationStore.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/svelte-urql/src/operationStore.ts b/packages/svelte-urql/src/operationStore.ts index ede59efaf1..6466a4fd22 100644 --- a/packages/svelte-urql/src/operationStore.ts +++ b/packages/svelte-urql/src/operationStore.ts @@ -49,8 +49,6 @@ export function operationStore( } as OperationStore; const store = writable(state); - const invalidate = store.set.bind(null, state); - let _internalUpdate = false; function set(value?: Partial) { @@ -82,7 +80,7 @@ export function operationStore( } _internalUpdate = false; - invalidate(); + store.set(state); } function update(fn: Updater): void { @@ -99,7 +97,7 @@ export function operationStore( get: () => internal[prop], set(value) { internal[prop] = value; - if (!_internalUpdate) invalidate(); + if (!_internalUpdate) store.set(state); }, }); } @@ -127,7 +125,7 @@ export function operationStore( get: () => internal[prop], set(value) { internal[prop] = value; - if (!_internalUpdate) invalidate(); + if (!_internalUpdate) store.set(state); }, }); } From c9fa6a63ad09ba479347bc6079b704546c817694 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 15:01:38 +0100 Subject: [PATCH 12/17] Fix how getters are defined to work around broken minification --- packages/svelte-urql/src/operationStore.ts | 38 +++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/svelte-urql/src/operationStore.ts b/packages/svelte-urql/src/operationStore.ts index 6466a4fd22..184acbfb30 100644 --- a/packages/svelte-urql/src/operationStore.ts +++ b/packages/svelte-urql/src/operationStore.ts @@ -48,17 +48,17 @@ export function operationStore( extensions: undefined, } as OperationStore; - const store = writable(state); + const svelteStore = writable(state); let _internalUpdate = false; - function set(value?: Partial) { + state.set = function set(value?: Partial) { if (!value) value = noop; _internalUpdate = true; if (process.env.NODE_ENV !== 'production') { if (!_storeUpdate.has(value!)) { for (const key in value) { - if (key !== 'query' && key !== 'variables') { + if (!(key in internal)) { throw new TypeError( 'It is not allowed to update result properties on an OperationStore.' ); @@ -80,32 +80,32 @@ export function operationStore( } _internalUpdate = false; - store.set(state); - } + svelteStore.set(state); + }; - function update(fn: Updater): void { - set(fn(state)); - } + state.update = function update(fn: Updater): void { + state.set(fn(state)); + }; - state.set = set; - state.update = update; - state.subscribe = store.subscribe; + state.subscribe = function subscribe(run, invalidate) { + return svelteStore.subscribe(run, invalidate); + }; - for (const prop in internal) { + Object.keys(internal).forEach(prop => { Object.defineProperty(state, prop, { configurable: false, get: () => internal[prop], set(value) { internal[prop] = value; - if (!_internalUpdate) store.set(state); + if (!_internalUpdate) svelteStore.set(state); }, }); - } + }); if (process.env.NODE_ENV !== 'production') { const result = { ...state }; - for (const prop in state) { + Object.keys(state).forEach(prop => { Object.defineProperty(result, prop, { configurable: false, get() { @@ -117,18 +117,18 @@ export function operationStore( ); }, }); - } + }); - for (const prop in internal) { + Object.keys(internal).forEach(prop => { Object.defineProperty(result, prop, { configurable: false, get: () => internal[prop], set(value) { internal[prop] = value; - if (!_internalUpdate) store.set(state); + if (!_internalUpdate) svelteStore.set(state); }, }); - } + }); return result; } From 78090d04705ed85c637218f9ae3a4786cfe2b578 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 18:11:23 +0100 Subject: [PATCH 13/17] Add Svelte docs sections to guide pages --- docs/advanced/subscriptions.md | 59 ++++++++- docs/basics/getting-started.md | 101 +++++++++++++++ docs/basics/mutations.md | 126 +++++++++++++++++++ docs/basics/queries.md | 217 ++++++++++++++++++++++++++++++++- 4 files changed, 501 insertions(+), 2 deletions(-) diff --git a/docs/advanced/subscriptions.md b/docs/advanced/subscriptions.md index 918eb6c1f2..e54d7c312a 100644 --- a/docs/advanced/subscriptions.md +++ b/docs/advanced/subscriptions.md @@ -72,7 +72,7 @@ we return to the `subscriptionExchange` inside `forwardSubscription`. ## React & Preact The `useSubscription` hooks comes with a similar API to `useQuery`, which [we've learned about in -the "Queries" page in the "Basics" section.](../basics/queries.md) +the "Queries" page in the "Basics" section.](../basics/queries.md#react--preact) Its usage is extremely similar in that it accepts options, which may contain `query` and `variables`. However, it also accepts a second argument, which is a reducer function, similar to @@ -130,6 +130,62 @@ the `handleSubscription` function. This works over time, so as new messages come in, we will append them to the list of previous messages. +## Svelte + +The `subscription` function in `@urql/svelte` comes with a similar API to `query`, which [we've +learned about in the "Queries" page in the "Basics" section.](../basics/queries.md#svelte) + +Its usage is extremely similar in that it accepts an `operationStore`, which will typically contain +our GraphQL subscription query. However, `subscription` also accepts a second argument, which is +a reducer function, similar to what you would pass to `Array.prototype.reduce`. + +It receives the previous set of data that this function has returned or `undefined`. +As the second argument, it receives the event that has come in from the subscription. +You can use this to accumulate the data over time, which is useful for a +list for example. + +In the following example, we create a subscription that informs us of +new messages. We will concatenate the incoming messages so that we +can display all messages that have come in over the subscription across +events. + +```js + + +{#if !$result.data} +

No new messages

+{:else} +
    + {#each $messages.data as message} +
  • {message.from}: "{message.text}"
  • + {/each} +
+{/if} + +``` + +As we can see, the `$result.data` is being updated and transformed by the `handleSubscription` +function. This works over time, so as new messages come in, we will append them to +the list of previous messages. + ## One-off Subscriptions When you're using subscriptions directly without `urql`'s framework bindings, you can use the `Client`'s `subscription` method for one-off subscriptions. This method is similar to the ones for mutations and subscriptions [that we've seen before on the "Core Package" page.](../concepts/core-package.md#one-off-queries-and-mutations) @@ -155,3 +211,4 @@ const { unsubscribe } = pipe( console.log(result); // { data: ... } }) ); +``` diff --git a/docs/basics/getting-started.md b/docs/basics/getting-started.md index d3ee1bda67..67342faf3a 100644 --- a/docs/basics/getting-started.md +++ b/docs/basics/getting-started.md @@ -95,3 +95,104 @@ const App = () => ( Now every component and element inside and under the `Provider` are able to use GraphQL queries that will be sent to our API. + +[On the next page we'll learn about executing "Queries".](./queries.md#react--preact) + +## Svelte + +This "Getting Started" guide covers how to install and set up `urql` and provide a `Client` for +Svelte. The `@urql/svelte` package, which provides bindings for Svelte, doesn't fundamentally +function differently from `@urql/preact` or `urql` and uses the same [Core Package and +`Client`](../concepts/core-package.md). + +### Installation + +Installing `@urql/svelte` is quick and no other packages are immediately necessary. + +```sh +yarn add @urql/svelte graphql +# or +npm install --save @urql/svelte graphql +``` + +Most libraries related to GraphQL also need the `graphql` package to be installed as a peer +dependency, so that they can adapt to your specific versioning requirements. That's why we'll need +to install `graphql` alongside `@urql/svelte`. + +Both the `@urql/svelte` and `graphql` packages follow [semantic versioning](https://semver.org) and +all `@urql/svelte` packages will define a range of compatible versions of `graphql`. Watch out +for breaking changes in the future however, in which case your package manager may warn you about +`graphql` being out of the defined peer dependency range. + +### Setting up the `Client` + +The `@urql/svelte` package exports a method called `createClient` which we can use to create +the GraphQL client. This central `Client` manages all of our GraphQL requests and results. + +```js +import { createClient } from '@urql/svelte'; + +const client = createClient({ + url: 'http://localhost:3000/graphql', +}); +``` + +At the bare minimum we'll need to pass an API's `url` when we create a `Client` to get started. + +Another common option is `fetchOptions`. This option allows us to customize the options that will be +passed to `fetch` when a request is sent to the given API `url`. We may pass in an options object or +a function returning an options object. + +In the following example we'll add a token to each `fetch` request that our `Client` sends to our +GraphQL API. + +```js +const client = createClient({ + url: 'http://localhost:3000/graphql', + fetchOptions: () => { + const token = getToken(); + return { + headers: { authorization: token ? `Bearer ${token}` : '' }, + }; + }, +}); +``` + +### Providing the `Client` + +To make use of the `Client` in Svelte we will have to provide it via the +[Context API](https://svelte.dev/tutorial/context-api). From a parent component to its child +components. This will share one `Client` with the rest of our app, if we for instance provide the +`Client` + +```html + +``` + +The `setClient` method internally calls [Svelte's `setContext` +function](https://svelte.dev/docs#setContext). The `@urql/svelte` package also exposes a `getClient` +function that uses [`getContext`](https://svelte.dev/docs#getContext) to retrieve the `Client` in +child components. This is used throughout `@urql/svelte`'s API. + +We can also use a convenience function, `initClient`. This function combines the `createClient` and +`setClient` calls into one. + +```html + +``` + +[On the next page we'll learn about executing "Queries".](./queries.md#svelte) diff --git a/docs/basics/mutations.md b/docs/basics/mutations.md index a84de89df0..42f6583253 100644 --- a/docs/basics/mutations.md +++ b/docs/basics/mutations.md @@ -100,3 +100,129 @@ it.](../api/urql.md#usemutation) [On the next page we'll learn about "Document Caching", `urql`'s default caching mechanism.](./document-caching.md) + +## Svelte + +This guide covers how to send mutations in Svelte using `@urql/svelte`'s `mutation` utility. +The `mutation` function isn't dissimilar from the `query` function but is triggered manually and +can accept a [`GraphQLRequest` object](../api/core.md#graphqlrequest) too while also supporting our +trusty `operationStore`. + +### Sending a mutation + +Let's again pick up an example with an imaginary GraphQL API for todo items, and dive into an +example! We'll set up a mutation that _updates_ a todo item's title. + +```html + +``` + +This small call to `mutation` accepts a `query` property (besides the `variables` property) and +returns an execute function. We've wrapped it in an `updateTodo` function to illustrate its usage. + +Unlike the `query` function, the `mutation` function doesn't start our mutation automatically. +Instead, mutations are started programmatically by calling the function they return. This function +also returns a promise so that we can use the mutation's result. + +### Using the mutation result + +When calling `mutateTodo` in our previous example, we start the mutation. To use the mutation's +result we actually have two options instead of one. + +The first option is to use the promise that the `mutation`'s execute function returns. This promise +will resolve to an `operationStore`, which is what we're used to from sending queries. Using this +store we can then read the mutation's `data` or `error`. + +```html + +``` + +Alternatively, we can pass `mutation` an `operationStore` directly. This allows us to use a +mutation's result in our component's UI more easily, without storing it ourselves. + +```html + + +{#if $updateTodoStore.data} Todo was updated! {/if} +``` + +### Handling mutation errors + +It's worth noting that the promise we receive when calling the execute function will never +reject. Instead it will always return a promise that resolves to an `operationStore`, even if the +mutation has failed. + +If you're checking for errors, you should use `operationStore.error` instead, which will be set +to a `CombinedError` when any kind of errors occurred while executing your mutation. +[Read more about errors on our "Errors" page.](./errors.md) + +```jsx +mutateTodo({ id, title: newTitle }).then(result => { + if (result.error) { + console.error('Oh no!', result.error); + } +}); +``` + +[On the next page we'll learn about "Document Caching", `urql`'s default caching +mechanism.](./document-caching.md) diff --git a/docs/basics/queries.md b/docs/basics/queries.md index aa0cecc525..e23931f788 100644 --- a/docs/basics/queries.md +++ b/docs/basics/queries.md @@ -192,4 +192,219 @@ when `pause` is set to `true`, which would usually stop all automatic queries. There are some more tricks we can use with `useQuery`. [Read more about its API in the API docs for it.](../api/urql.md#usequery) -[On the next page we'll learn about "Mutations" rather than Queries.](./mutations.md) +[On the next page we'll learn about "Mutations" rather than Queries.](./mutations.md#react--preact) + +## Svelte + +This guide covers how to query data with Svelte with our `Client` now fully set up and provided via +the Context API. We'll implement queries using the `operationStore` and the `query` function from +`@urql/svelte`. + +The `operationStore` function creates a [Svelte Writable store](https://svelte.dev/docs#writable). +You can use it to initialise a data container in `urql`. This store holds on to our query inputs, +like the GraphQL query and variables, which we can change to launch new queries, and also exposes +the query's eventual result, which we can then observe. + +### Run a first query + +For the following examples, we'll imagine that we're querying data from a GraphQL API that contains +todo items. Let's dive right into it! + +```html + + +{#if $todos.fetching} +

Loading...

+{:else if $todos.error} +

Oh no... {$todos.error.message}

+{:else} +
    + {#each $todos.data.todos as todo} +
  • {todo.title}
  • + {/each} +
+{/if} +``` + +Here we have implemented our first GraphQL query to fetch todos. We're first creating an +`operationStore` which holds on to our `query` and are then passing the store to the `query` +function, which starts the GraphQL query. + +The `todos` store can now be used like any other Svelte store using a +[reactive auto-subscription](https://svelte.dev/tutorial/auto-subscriptions) in Svelte. This means +that we prefix `$todos` with a dollar symbol, which automatically subscribes us to its changes. + +The `query` function accepts our store and starts using the `Client` to execute our query. It may +only be called once for a store and lives alongside the component's lifecycle. It will automatically +read changes on the `operationStore` and will update our query and results accordingly. + +### Variables + +Typically we'll also need to pass variables to our queries, for instance, if we are dealing with +pagination. For this purpose the `operationStore` also accepts a `variables` argument, which we can +use to supply variables to our query. + +```html + + +... +``` + +As when we're sending GraphQL queries manually using `fetch`, the variables will be attached to the +`POST` request body that is sent to our GraphQL API. + +The `operationStore` also supports being actively changed. This will hook into Svelte's reactivity +model as well and cause the `query` utility to start a new operation. + +```html + + + +``` + +The `operationStore` provides getters too so it's also possible for us to pass `todos` around and +update `todos.variables` or `todos.query` directly. Both, updating `todos.variables` and +`$todos.variables` in a component for instance, will cause `query` to pick up the update and execute +our changes. + +### Request Policies + +The `operationStore` also accepts another argument apart from `query` and `variables`. Optionally +you may pass a third argument, [the `context` object](../api/core.md#operationcontext). The arguably +most interesting option the `context` may contain is `requestPolicy`. + +The `requestPolicy` option determines how results are retrieved from our `Client`'s cache. By +default this is set to `cache-first`, which means that we prefer to get results from our cache, but +are falling back to sending an API request. + +In total there are four different policies that we can use: + +- `cache-first` (the default) prefers cached results and falls back to sending an API request when + no prior result is cached. +- `cache-and-network` returns cached results but also always sends an API request, which is perfect + for displaying data quickly while keeping it up-to-date. +- `network-only` will always send an API request and will ignore cached results. +- `cache-only` will always return cached results or `null`. + +The `cache-and-network` policy is particularly useful, since it allows us to display data instantly +if it has been cached, but also refreshes data in our cache in the background. This means though +that `fetching` will be `false` for cached results although an API request may still be ongoing in +the background. + +For this reason there's another field on results, `result.stale`, which indicates that the cached +result is either outdated or that another request is being sent in the background. + +```html + + +... +``` + +As we can see, the `requestPolicy` is easily changed here and we can read our `context` option back +from `todos.context`, just as we can check `todos.query` and `todos.variables`. Updating +`operationStore.context` can be very useful to also refetch queries, as we'll see in the next +section. + +[You can learn more about request policies on the API docs.](../api/core.md#requestpolicy) + +### Reexecuting Queries + +The default caching approach in `@urql/svelte` typically takes care of updating queries on the fly +quite well and does so automatically. Sometimes it may be necessary though to refetch data and to +execute a query with a different `context`. Triggering a query programmatically may be useful in a +couple of cases. It can for instance be used to refresh data that is currently being displayed. + +We can trigger a new query update by changing out the `context` of our `operationStore`. + +```html + +``` + +Calling `refresh` in the above example will execute the query again forcefully, and will skip the +cache, since we're passing `requestPolicy: 'network-only'`. + +### Reading on + +There are some more tricks we can use with `operationStore`. +[Read more about its API in the API docs for it.](../api/svelte.md#operationStore) + +[On the next page we'll learn about "Mutations" rather than Queries.](./mutations.md#svelte) From 7f176a7910dbdf47249cd9f86f79f1244c22e3c1 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 18:39:25 +0100 Subject: [PATCH 14/17] Add Svelte API docs --- docs/api/README.md | 1 + docs/api/auth-exchange.md | 2 +- docs/api/execute-exchange.md | 2 +- docs/api/graphcache.md | 2 +- docs/api/multipart-fetch-exchange.md | 2 +- docs/api/persisted-fetch-exchange.md | 2 +- docs/api/refocus-exchange.md | 2 +- docs/api/request-policy-exchange.md | 2 +- docs/api/retry-exchange.md | 2 +- docs/api/svelte.md | 106 +++++++++++++++++++++++++ packages/svelte-urql/src/operations.ts | 9 ++- 11 files changed, 122 insertions(+), 10 deletions(-) create mode 100644 docs/api/svelte.md diff --git a/docs/api/README.md b/docs/api/README.md index eea0220f2d..2f8d1ad777 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -14,6 +14,7 @@ more about the core package on the "Core Package" page.](../concepts/core-packag - [`@urql/core` API docs](./core.md) - [`urql` React API docs](./urql.md) - [`@urql/preact` Preact API docs](./preact.md) +- [`@urql/svelte` Svelte API docs](./svelte.md) - [`@urql/exchange-graphcache` API docs](./graphcache.md) - [`@urql/exchange-retry` API docs](./retry-exchange.md) - [`@urql/exchange-execute` API docs](./execute-exchange.md) diff --git a/docs/api/auth-exchange.md b/docs/api/auth-exchange.md index ee3cb8f12e..5b69d5e147 100644 --- a/docs/api/auth-exchange.md +++ b/docs/api/auth-exchange.md @@ -1,6 +1,6 @@ --- title: '@urql/exchange-auth' -order: 9 +order: 10 --- # Authentication Exchange diff --git a/docs/api/execute-exchange.md b/docs/api/execute-exchange.md index b3635a2436..e014ce93fd 100644 --- a/docs/api/execute-exchange.md +++ b/docs/api/execute-exchange.md @@ -1,6 +1,6 @@ --- title: '@urql/exchange-execute' -order: 5 +order: 6 --- # Execute Exchange diff --git a/docs/api/graphcache.md b/docs/api/graphcache.md index 56560e381f..4b492e9473 100644 --- a/docs/api/graphcache.md +++ b/docs/api/graphcache.md @@ -1,6 +1,6 @@ --- title: '@urql/exchange-graphcache' -order: 3 +order: 4 --- # @urql/exchange-graphcache diff --git a/docs/api/multipart-fetch-exchange.md b/docs/api/multipart-fetch-exchange.md index 074f254a4d..e54e796129 100644 --- a/docs/api/multipart-fetch-exchange.md +++ b/docs/api/multipart-fetch-exchange.md @@ -1,6 +1,6 @@ --- title: '@urql/exchange-multipart-fetch' -order: 6 +order: 7 --- # Multipart Fetch Exchange diff --git a/docs/api/persisted-fetch-exchange.md b/docs/api/persisted-fetch-exchange.md index cd86b5c57b..28c33ef6de 100644 --- a/docs/api/persisted-fetch-exchange.md +++ b/docs/api/persisted-fetch-exchange.md @@ -1,6 +1,6 @@ --- title: '@urql/exchange-persisted-fetch' -order: 7 +order: 8 --- # Persisted Fetch Exchange diff --git a/docs/api/refocus-exchange.md b/docs/api/refocus-exchange.md index 268f0b3751..2323cd8880 100644 --- a/docs/api/refocus-exchange.md +++ b/docs/api/refocus-exchange.md @@ -1,6 +1,6 @@ --- title: '@urql/exchange-refocus' -order: 10 +order: 11 --- # Refocus exchange diff --git a/docs/api/request-policy-exchange.md b/docs/api/request-policy-exchange.md index a086f32110..5f66637506 100644 --- a/docs/api/request-policy-exchange.md +++ b/docs/api/request-policy-exchange.md @@ -1,6 +1,6 @@ --- title: '@urql/exchange-request-policy' -order: 8 +order: 9 --- # Request Policy Exchange diff --git a/docs/api/retry-exchange.md b/docs/api/retry-exchange.md index dc594ee492..82cb5fb0a6 100644 --- a/docs/api/retry-exchange.md +++ b/docs/api/retry-exchange.md @@ -1,6 +1,6 @@ --- title: '@urql/exchange-retry' -order: 4 +order: 5 --- # Retry Exchange diff --git a/docs/api/svelte.md b/docs/api/svelte.md new file mode 100644 index 0000000000..ffcc42672a --- /dev/null +++ b/docs/api/svelte.md @@ -0,0 +1,106 @@ +--- +title: @urql/svelte +order: 3 +--- + +# Svelte API + +## operationStore + +Accepts three arguments as inputs, where only the first one — `query` — is required. + +| Argument | Type | Description | +| --------- | ------------------------ | ---------------------------------------------------------------------------------- | +| query | `string \| DocumentNode` | The query to be executed. Accepts as a plain string query or GraphQL DocumentNode. | +| variables | `?object` | The variables to be used with the GraphQL request. | +| context | `?object` | Holds the contextual information for the query. | + +This is a [Svelte Writable Store](https://svelte.dev/docs#writable) that is used by other utilities +listed in these docs to read [`Operation` inputs](./core.md#operation) from and write +[`OperationResult` outputs](./core.md#operationresult) to. + +The store has several properties on its value. The **writable properties** of it are inputs that are +used by either [`query`](#query), [`mutation`](#mutation), or [`subscription`](#subscription) to +create an [`Operation`](./core.md#operation) to execute. These are `query`, `variables`, and +`context`; the same properties that the `operationStore` accepts as arguments on creation. + +Furthermore the store exposes some **readonly properties** which represent the operation's progress +and [result](./core.md#operationresult). + +| Prop | Type | Description | +| ----------------------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| data | `?any` | Data returned by the specified query | +| error | `?CombinedError` | A [`CombinedError`](./core.md#combinederror) instances that wraps network or `GraphQLError`s (if any) | +| extensions | `?Record` | Extensions that the GraphQL server may have returned. | +| stale | `boolean` | A flag that may be set to `true` by exchanges to indicate that the `data` is incomplete or out-of-date, and that the result will be updated soon. | +| fetching | `boolean` | A flag that indicates whether the operation is currently | +| in progress, which means that the `data` and `error` is out-of-date for the given inputs. | + +All of the writable properties are updatable either via the common Svelte Writable's `set` or +`update` methods or directly. The `operationStore` exposes setters for the writable properties which +will automatically update the store and notify reactive subscribers. + +In development, trying to update the _readonly_ properties directly or via the `set` or `update` +method will result in a `TypeError` being thrown. + +[Read more about `writable` stores on the Svelte API docs.](https://svelte.dev/docs#writable) + +## query + +The `query` utility function only accepts an `operationStore` as its only argument. Per +`operationStore` it should only be called once per component as it lives alongside the component and +hooks into its `onDestroy` lifecycle method. This means that we must avoid passing a reactive +variable to it, and instead must pass the raw `operationStore`. + +This function will return the `operationStore` itself that has been passed. + +[Read more about how to use the `query` API on the "Queries" page.](../basics/queries.md#svelte) + +## subscription + +The `subscription` utility function accepts an `operationStore` as its first argument, like the +[`query` function](#query). It should also per `operationStore` be called once per component. + +The function also optionally accepts a second argument, a `handler` function. This function has the +following type signature: + +```js +type SubscriptionHandler = (previousData: R | undefined, data: T) => R; +``` + +This function will be called with the previous data (or `undefined`) and the new data that's +incoming from a subscription event, and may be used to "reduce" the data over time, altering the +value of `result.data`. + +`subscription` itself will return the `operationStore` that has been passed when called. + +[Read more about how to use the `subscription` API on the "Subscriptions" +page.](../advanced/subscriptions.md#svelte) + +## mutation + +The `mutation` utility function either accepts an `operationStore` as its only argument or an object +containing `query`, `variables`, and `context` properties. When it receives the latter it will +create an `operationStore` automatically. + +The function will return an `executeMutation` callback, which can be used to trigger the mutation. +This callback optionally accepts a `variables` argument and a `context` argument of type +[`Partial`](./core.md#operationcontext). If these arguments are passed, they will +automatically update the `operationStore` before starting the mutation. + +The `executeMutation` callback will return a promise which resolves to the `operationStore` once the +mutation has been completed. + +[Read more about how to use the `mutation` API on the "Mutations" +page.](../basics/mutations.md#svelte) + +## Context API + +In Svelte the [`Client`](./core.md#client) is passed around using [Svelte's Context +API](https://svelte.dev/tutorial/context-api). `@urql/svelte` wraps around Svelte's +[`setContext`](https://svelte.dev/docs#setContext) and +[`getContext`](https://svelte.dev/docs#getContext) functions and exposes: + +- `setClient` +- `getClient` +- `initClient` (a shortcut for `createClient` + `setClient`) diff --git a/packages/svelte-urql/src/operations.ts b/packages/svelte-urql/src/operations.ts index c2bf500a22..688c187265 100644 --- a/packages/svelte-urql/src/operations.ts +++ b/packages/svelte-urql/src/operations.ts @@ -126,13 +126,18 @@ export function mutation( : (input as OperationStore); return (vars, context) => { - const update = { fetching: true, variables: vars || store.variables }; + const update = { + fetching: true, + variables: vars || store.variables, + context: context || store.context, + }; + _markStoreUpdate(update); store.set(update); return new Promise(resolve => { client - .mutation(store.query, store.variables, context) + .mutation(store.query, store.variables, store.context) .toPromise() .then(result => { const update = { fetching: false, ...result }; From a8c34186c9669168d9d8d42dbabdefb0e5023458 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 18:42:48 +0100 Subject: [PATCH 15/17] Add changeset --- .changeset/real-horses-cough.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/real-horses-cough.md diff --git a/.changeset/real-horses-cough.md b/.changeset/real-horses-cough.md new file mode 100644 index 0000000000..ab3ab7846c --- /dev/null +++ b/.changeset/real-horses-cough.md @@ -0,0 +1,12 @@ +--- +'@urql/svelte': major +--- + +Reimplement the `@urql/svelte` API, which is now marked as stable. +The new `@urql/svelte` API features the `query`, `mutation`, and `subscription` utilities, which are +called as part of a component's normal lifecycle and accept `operationStore` stores. These are +writable stores that encapsulate both a GraphQL operation's inputs and outputs (the result)! + +Learn more about how to use `@urql/svelte` [in our new API +docs](https://formidable.com/open-source/urql/docs/api/svelte/) or starting from the [Basics +pages.](https://formidable.com/open-source/urql/docs/basics/) From 40aa1faaf0165a60f5b59a7829b44f282512df60 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 30 Sep 2020 18:49:12 +0100 Subject: [PATCH 16/17] Fix Svelte API docs title --- docs/api/svelte.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/svelte.md b/docs/api/svelte.md index ffcc42672a..029da6eb2f 100644 --- a/docs/api/svelte.md +++ b/docs/api/svelte.md @@ -1,5 +1,5 @@ --- -title: @urql/svelte +title: '@urql/svelte' order: 3 --- From 3c760185aa3eb16aee4736d96bcf6e214671de70 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 1 Oct 2020 12:19:25 +0100 Subject: [PATCH 17/17] Rename noop variable --- packages/svelte-urql/src/operationStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte-urql/src/operationStore.ts b/packages/svelte-urql/src/operationStore.ts index 184acbfb30..e2009580ba 100644 --- a/packages/svelte-urql/src/operationStore.ts +++ b/packages/svelte-urql/src/operationStore.ts @@ -4,7 +4,7 @@ import { OperationContext, CombinedError } from '@urql/core'; import { _storeUpdate } from './internal'; -const noop = Object.create(null); +const emptyUpdate = Object.create(null); type Updater = (value: T) => T; @@ -52,7 +52,7 @@ export function operationStore( let _internalUpdate = false; state.set = function set(value?: Partial) { - if (!value) value = noop; + if (!value) value = emptyUpdate; _internalUpdate = true; if (process.env.NODE_ENV !== 'production') {