@graphcommerce/graphql 10.1.0-canary.9 → 11.0.0-canary.46
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 +88 -0
- package/apolloTypeOverrides.d.ts +34 -0
- package/components/GraphQLProvider/GraphQLProvider.tsx +27 -5
- package/index.ts +3 -0
- package/package.json +7 -7
- package/utils/renewSignal.ts +121 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,93 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 11.0.0-canary.46
|
|
4
|
+
|
|
5
|
+
## 11.0.0-canary.45
|
|
6
|
+
|
|
7
|
+
## 10.1.0-canary.44
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#2662](https://github.com/graphcommerce-org/graphcommerce/pull/2662) [`54e167f`](https://github.com/graphcommerce-org/graphcommerce/commit/54e167f13b883fbe24e954c6c093097c0815263f) - Publishing content now pushes a renew signal through the Next.js incremental cache, so every server invalidates at once instead of each polling `cdn/spaces/me` on a 60 second interval — `storyblok.cacheVersionTtl` therefore defaults to `3600` as a failsafe. ([@paales](https://github.com/paales))
|
|
12
|
+
|
|
13
|
+
## 10.1.0-canary.43
|
|
14
|
+
|
|
15
|
+
## 10.1.0-canary.42
|
|
16
|
+
|
|
17
|
+
## 10.1.0-canary.41
|
|
18
|
+
|
|
19
|
+
## 10.1.0-canary.40
|
|
20
|
+
|
|
21
|
+
## 10.1.0-canary.39
|
|
22
|
+
|
|
23
|
+
## 10.1.0-canary.38
|
|
24
|
+
|
|
25
|
+
## 10.1.0-canary.37
|
|
26
|
+
|
|
27
|
+
## 10.1.0-canary.36
|
|
28
|
+
|
|
29
|
+
## 10.1.0-canary.35
|
|
30
|
+
|
|
31
|
+
## 10.1.0-canary.34
|
|
32
|
+
|
|
33
|
+
### Minor Changes
|
|
34
|
+
|
|
35
|
+
- [#2646](https://github.com/graphcommerce-org/graphcommerce/pull/2646) [`35707a1`](https://github.com/graphcommerce-org/graphcommerce/commit/35707a1f68a6b8c97de5d22221a7c9229961cd7a) - Add a `terminatingLink` prop to `GraphQLProvider` to override the terminating link at the tail of the Apollo link chain (by default the `HttpLink` to the Mesh backend).
|
|
36
|
+
|
|
37
|
+
Because the terminating link runs after every context-setting link (customer auth token, store, cache-id, header links), this lets you route specific operations to a different transport while still inheriting all request headers. The motivating case is file uploads: `File`/`Blob` variables must be sent as a `multipart/form-data` request (e.g. via `apollo-upload-client`'s `UploadHttpLink`), which the default `HttpLink` cannot serialize. Previously such an upload link had to be prepended via `links`, where it terminated _before_ the auth/header links could run — dropping the customer token from multipart requests, so a logged-in customer's cart mutations were rejected. Supplying the upload-aware split as `terminatingLink` keeps it at the tail, so uploads inherit the token like any other operation. ([@paales](https://github.com/paales))
|
|
38
|
+
|
|
39
|
+
## 10.1.0-canary.33
|
|
40
|
+
|
|
41
|
+
## 10.1.0-canary.32
|
|
42
|
+
|
|
43
|
+
## 10.1.0-canary.31
|
|
44
|
+
|
|
45
|
+
## 10.1.0-canary.30
|
|
46
|
+
|
|
47
|
+
## 10.1.0-canary.29
|
|
48
|
+
|
|
49
|
+
## 10.1.0-canary.28
|
|
50
|
+
|
|
51
|
+
## 10.1.0-canary.27
|
|
52
|
+
|
|
53
|
+
## 10.1.0-canary.26
|
|
54
|
+
|
|
55
|
+
## 10.1.0-canary.25
|
|
56
|
+
|
|
57
|
+
## 10.1.0-canary.24
|
|
58
|
+
|
|
59
|
+
## 10.1.0-canary.23
|
|
60
|
+
|
|
61
|
+
### Patch Changes
|
|
62
|
+
|
|
63
|
+
- [#2634](https://github.com/graphcommerce-org/graphcommerce/pull/2634) [`06082ad`](https://github.com/graphcommerce-org/graphcommerce/commit/06082ad47a59217fbdfb24f1a60411e4d0eecd9b) - Make GraphCommerce compatible with Apollo Client 4.2+ by augmenting Apollo's `DefaultOptions` type with the `preview` extension and the SSR clients' `errorPolicy: 'all'` default. ([@bramvanderholst](https://github.com/bramvanderholst))
|
|
64
|
+
|
|
65
|
+
## 10.1.0-canary.22
|
|
66
|
+
|
|
67
|
+
## 10.1.0-canary.21
|
|
68
|
+
|
|
69
|
+
## 10.1.0-canary.20
|
|
70
|
+
|
|
71
|
+
## 10.1.0-canary.19
|
|
72
|
+
|
|
73
|
+
## 10.1.0-canary.18
|
|
74
|
+
|
|
75
|
+
## 10.1.0-canary.17
|
|
76
|
+
|
|
77
|
+
## 10.1.0-canary.16
|
|
78
|
+
|
|
79
|
+
## 10.1.0-canary.15
|
|
80
|
+
|
|
81
|
+
## 10.1.0-canary.14
|
|
82
|
+
|
|
83
|
+
## 10.1.0-canary.13
|
|
84
|
+
|
|
85
|
+
## 10.1.0-canary.12
|
|
86
|
+
|
|
87
|
+
## 10.1.0-canary.11
|
|
88
|
+
|
|
89
|
+
## 10.1.0-canary.10
|
|
90
|
+
|
|
3
91
|
## 10.1.0-canary.9
|
|
4
92
|
|
|
5
93
|
## 10.1.0-canary.8
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apollo Client 4.2+ type augmentations for the GraphCommerce setup.
|
|
3
|
+
*
|
|
4
|
+
* - `DefaultOptions.Input.preview` declares the `preview` extension GraphCommerce stuffs into
|
|
5
|
+
* defaultOptions so it's part of the type instead of needing an `as unknown` cast at every
|
|
6
|
+
* client-construction site.
|
|
7
|
+
* - `DeclareDefaultOptions.Query.errorPolicy: 'all'` opts the SSR clients into errorPolicy 'all'
|
|
8
|
+
* so partial GraphQL responses are returned alongside errors instead of throwing on every
|
|
9
|
+
* field-level error during SSG.
|
|
10
|
+
* - `signatureStyle: 'classic'` keeps the pre-4.2 query result typing (`T` instead of
|
|
11
|
+
* `DeepPartial<T> | undefined`) so the codebase doesn't need an app-wide migration to handle
|
|
12
|
+
* the now-correctly-modelled partial data. This is a deliberate trade-off — TS doesn't model
|
|
13
|
+
* the runtime reality of errorPolicy 'all', but it keeps consumer code ergonomic.
|
|
14
|
+
*/
|
|
15
|
+
import '@apollo/client'
|
|
16
|
+
|
|
17
|
+
declare module '@apollo/client' {
|
|
18
|
+
namespace ApolloClient {
|
|
19
|
+
namespace DefaultOptions {
|
|
20
|
+
interface Input {
|
|
21
|
+
preview: unknown
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
namespace DeclareDefaultOptions {
|
|
25
|
+
interface Query {
|
|
26
|
+
errorPolicy?: 'all'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TypeOverrides {
|
|
32
|
+
signatureStyle: 'classic'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -20,7 +20,27 @@ export const globalApolloClient: { current: ApolloClient | null } = {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export type GraphQLProviderProps = AppProps &
|
|
23
|
-
Omit<ApolloClientConfigInput, 'storefront'> & {
|
|
23
|
+
Omit<ApolloClientConfigInput, 'storefront'> & {
|
|
24
|
+
children: React.ReactNode
|
|
25
|
+
/**
|
|
26
|
+
* Overrides the terminating link at the tail of the Apollo link chain (by default the `HttpLink`
|
|
27
|
+
* connection to the Mesh backend). Because it sits *after* every context-setting link (customer
|
|
28
|
+
* auth token, store, cache-id, header links), use it to route specific operations to a different
|
|
29
|
+
* transport while still inheriting all request headers.
|
|
30
|
+
*
|
|
31
|
+
* The motivating case is file uploads: `File`/`Blob` variables must be sent as a
|
|
32
|
+
* `multipart/form-data` request (e.g. via `apollo-upload-client`'s `UploadHttpLink`), which the
|
|
33
|
+
* default `HttpLink` cannot serialize. Providing the upload-aware split here keeps it at the tail
|
|
34
|
+
* of the chain, so uploads still pick up the customer token instead of going out unauthenticated.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* const terminatingLink = ApolloLink.split(hasUploadFiles, uploadHttpLink, httpLink)
|
|
39
|
+
* <GraphQLProvider {...props} terminatingLink={terminatingLink} />
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
terminatingLink?: ApolloLink
|
|
43
|
+
}
|
|
24
44
|
|
|
25
45
|
/**
|
|
26
46
|
* The GraphQLProvider allows us to configure the ApolloClient and provide it to the rest of the
|
|
@@ -29,7 +49,7 @@ export type GraphQLProviderProps = AppProps &
|
|
|
29
49
|
* Take a look at the props to see possible customization options.
|
|
30
50
|
*/
|
|
31
51
|
export function GraphQLProvider(props: GraphQLProviderProps) {
|
|
32
|
-
const { children, links, migrations, policies, pageProps, router } = props
|
|
52
|
+
const { children, links, migrations, policies, pageProps, router, terminatingLink } = props
|
|
33
53
|
const state = (pageProps as { apolloState?: unknown }).apolloState
|
|
34
54
|
|
|
35
55
|
const stateRef = useRef(state)
|
|
@@ -58,8 +78,10 @@ export function GraphQLProvider(props: GraphQLProviderProps) {
|
|
|
58
78
|
const link = ApolloLink.from([
|
|
59
79
|
...(typeof window === 'undefined' ? [errorLink, measurePerformanceLink] : []),
|
|
60
80
|
...config.current.links,
|
|
61
|
-
// The actual Http connection to the Mesh backend.
|
|
62
|
-
|
|
81
|
+
// The actual Http connection to the Mesh backend. Overridable via `terminatingLink` so
|
|
82
|
+
// e.g. multipart file uploads can be routed to a different transport while still inheriting
|
|
83
|
+
// every header set by the links above.
|
|
84
|
+
terminatingLink ?? new HttpLink({ uri: '/api/graphql', credentials: 'same-origin' }),
|
|
63
85
|
])
|
|
64
86
|
|
|
65
87
|
const cache = createCache()
|
|
@@ -76,7 +98,7 @@ export function GraphQLProvider(props: GraphQLProviderProps) {
|
|
|
76
98
|
preview: {
|
|
77
99
|
preview: router.isPreview,
|
|
78
100
|
} as PreviewConfig,
|
|
79
|
-
}
|
|
101
|
+
},
|
|
80
102
|
localState: new LocalState({}),
|
|
81
103
|
})
|
|
82
104
|
})
|
package/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/// <reference path="./apolloTypeOverrides.d.ts" />
|
|
2
|
+
|
|
1
3
|
export * from './apollo'
|
|
2
4
|
export * from './components/GraphQLProvider'
|
|
3
5
|
export { default as fragments } from './generated/fragments.json'
|
|
@@ -5,6 +7,7 @@ export * from './generated/types'
|
|
|
5
7
|
export * from './config'
|
|
6
8
|
export * from './utils/getPreviewData'
|
|
7
9
|
export * from './utils/cachePolicy'
|
|
10
|
+
export * from './utils/renewSignal'
|
|
8
11
|
export * from './components/PrivateQueryMask/PrivateQueryMask'
|
|
9
12
|
export * from './hooks/usePrivateQueryContext'
|
|
10
13
|
export * from './hooks/usePrivateQuery'
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@graphcommerce/graphql",
|
|
3
3
|
"homepage": "https://www.graphcommerce.org/",
|
|
4
4
|
"repository": "github:graphcommerce-org/graphcommerce",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "11.0.0-canary.46",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"main": "index.ts",
|
|
8
8
|
"prettier": "@graphcommerce/prettier-config-pwa",
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"rxjs": "^7.8.2"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@graphcommerce/eslint-config-pwa": "^
|
|
32
|
-
"@graphcommerce/graphql-codegen-near-operation-file": "
|
|
33
|
-
"@graphcommerce/graphql-codegen-relay-optimizer-plugin": "
|
|
34
|
-
"@graphcommerce/next-config": "^
|
|
35
|
-
"@graphcommerce/prettier-config-pwa": "^
|
|
36
|
-
"@graphcommerce/typescript-config-pwa": "^
|
|
31
|
+
"@graphcommerce/eslint-config-pwa": "^11.0.0-canary.46",
|
|
32
|
+
"@graphcommerce/graphql-codegen-near-operation-file": "11.0.0-canary.46",
|
|
33
|
+
"@graphcommerce/graphql-codegen-relay-optimizer-plugin": "11.0.0-canary.46",
|
|
34
|
+
"@graphcommerce/next-config": "^11.0.0-canary.46",
|
|
35
|
+
"@graphcommerce/prettier-config-pwa": "^11.0.0-canary.46",
|
|
36
|
+
"@graphcommerce/typescript-config-pwa": "^11.0.0-canary.46",
|
|
37
37
|
"@graphql-mesh/plugin-http-details-extensions": "*",
|
|
38
38
|
"react": "^19.2.0",
|
|
39
39
|
"react-dom": "^19.2.0"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deployment-wide "content was published" signal, carried through Next.js' incremental cache so
|
|
3
|
+
* it reaches every server that shares a `cacheHandler`.
|
|
4
|
+
*
|
|
5
|
+
* Caches that are built per process — an SSR Apollo client's `InMemoryCache`, a CMS client's pinned
|
|
6
|
+
* cache-version — are invalidated by a webhook that only ever reaches one server. This is how the
|
|
7
|
+
* other servers find out, without any of them polling an upstream API to ask whether anything
|
|
8
|
+
* changed.
|
|
9
|
+
*
|
|
10
|
+
* Two Next.js internals are involved. `globalThis.__incrementalCache` is set per incoming request
|
|
11
|
+
* before route handling, so it is available inside an API route and inside ISR regeneration, but
|
|
12
|
+
* *not* in middleware or the edge runtime, which construct their own instance. The entry is written
|
|
13
|
+
* as `kind: 'FETCH'` with `fetchCache: true`, the one path that stores an arbitrary key verbatim
|
|
14
|
+
* instead of running it through `normalizePagePath()`.
|
|
15
|
+
*
|
|
16
|
+
* Both are unstable API. If either changes shape, reads and writes fail closed: the signal stays at
|
|
17
|
+
* its last known value and callers fall back to whatever time-based failsafe they have. The call
|
|
18
|
+
* shape does drift — `revalidate` became `cacheControl` between Next 14 and 15 — so it needs
|
|
19
|
+
* re-testing per major.
|
|
20
|
+
*
|
|
21
|
+
* Deployments on Next's default `FileSystemCache` must set `cacheMaxMemorySize: 0`; its in-memory
|
|
22
|
+
* LRU sits in front of the shared layer and would answer every read from the writing process' own
|
|
23
|
+
* memory. Custom cache handlers have no such layer.
|
|
24
|
+
*/
|
|
25
|
+
const RENEW_SIGNAL_KEY = 'gc:signal:content-renew'
|
|
26
|
+
const RENEW_SIGNAL_POLL_MS = 1000
|
|
27
|
+
const RENEW_SIGNAL_REVALIDATE = 60 * 60 * 24 * 30
|
|
28
|
+
|
|
29
|
+
type IncrementalCacheLike = {
|
|
30
|
+
get: (key: string, ctx: unknown) => Promise<{ value?: { data?: { body?: string } } } | null>
|
|
31
|
+
set: (key: string, data: unknown, ctx: unknown) => Promise<void>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Undefined outside a Next.js request, in the browser bundle, and on the edge runtime. */
|
|
35
|
+
function incrementalCache(): IncrementalCacheLike | undefined {
|
|
36
|
+
return (globalThis as { __incrementalCache?: IncrementalCacheLike }).__incrementalCache
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `undefined` until the first read completes — distinct from "read, and nothing was published". */
|
|
40
|
+
let signalValue: number | undefined
|
|
41
|
+
let signalReadAt = 0
|
|
42
|
+
let inFlight: Promise<number | undefined> | undefined
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Publish the signal. Call this from the webhook that learns content changed.
|
|
46
|
+
*
|
|
47
|
+
* The value is a millisecond timestamp: only its ordering matters, never its absolute value.
|
|
48
|
+
*/
|
|
49
|
+
export async function publishRenewSignal(): Promise<void> {
|
|
50
|
+
const cache = incrementalCache()
|
|
51
|
+
if (!cache) return
|
|
52
|
+
try {
|
|
53
|
+
await cache.set(
|
|
54
|
+
RENEW_SIGNAL_KEY,
|
|
55
|
+
{
|
|
56
|
+
kind: 'FETCH',
|
|
57
|
+
data: { headers: {}, body: String(Date.now()), url: '', status: 200 },
|
|
58
|
+
tags: [],
|
|
59
|
+
revalidate: RENEW_SIGNAL_REVALIDATE,
|
|
60
|
+
},
|
|
61
|
+
{ fetchCache: true, tags: [] },
|
|
62
|
+
)
|
|
63
|
+
} catch {
|
|
64
|
+
// Unsupported or unwritable cache handler.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function readRenewSignal(): Promise<number | undefined> {
|
|
69
|
+
const cache = incrementalCache()
|
|
70
|
+
if (!cache) return signalValue
|
|
71
|
+
try {
|
|
72
|
+
const entry = await cache.get(RENEW_SIGNAL_KEY, {
|
|
73
|
+
kind: 'FETCH',
|
|
74
|
+
tags: [],
|
|
75
|
+
softTags: [],
|
|
76
|
+
revalidate: RENEW_SIGNAL_REVALIDATE,
|
|
77
|
+
})
|
|
78
|
+
const value = Number(entry?.value?.data?.body)
|
|
79
|
+
// An absent entry means nothing has been published yet, which is a known state, not an unknown
|
|
80
|
+
// one — record it so cold-start callers stop treating the signal as unreadable.
|
|
81
|
+
signalValue = Number.isFinite(value) && value > 0 ? value : (signalValue ?? 0)
|
|
82
|
+
} catch {
|
|
83
|
+
// A cache handler that cannot serve this entry. Leave the signal as it was.
|
|
84
|
+
}
|
|
85
|
+
return signalValue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read the signal, at most once per {@link RENEW_SIGNAL_POLL_MS}. Never an upstream API request.
|
|
90
|
+
*
|
|
91
|
+
* Concurrent callers share one read: on a fresh pod every in-flight `getStaticProps` hits this at
|
|
92
|
+
* once, and they should not each open their own.
|
|
93
|
+
*/
|
|
94
|
+
export async function refreshRenewSignal(): Promise<number | undefined> {
|
|
95
|
+
const now = Date.now()
|
|
96
|
+
if (signalValue !== undefined && now - signalReadAt < RENEW_SIGNAL_POLL_MS) return signalValue
|
|
97
|
+
if (!inFlight) {
|
|
98
|
+
signalReadAt = now
|
|
99
|
+
inFlight = readRenewSignal().finally(() => {
|
|
100
|
+
inFlight = undefined
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
return inFlight
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Last known signal value, or `undefined` when it has not been read yet.
|
|
108
|
+
*
|
|
109
|
+
* For callers that cannot await one: `graphqlSsrClient()` is synchronous and called as
|
|
110
|
+
* `const client = graphqlSsrClient(context)` throughout `getStaticProps`, so making the read
|
|
111
|
+
* awaited would mean making every consumer async. It gets the value from the previous refresh and
|
|
112
|
+
* schedules the next one, bounding staleness at one poll interval plus one call.
|
|
113
|
+
*
|
|
114
|
+
* `undefined` must not be read as "nothing published" — on a fresh pod, which is every pod right
|
|
115
|
+
* after a deploy, that would silently skip the first invalidation. Callers treat it as unknown and
|
|
116
|
+
* fall back to their own failsafe.
|
|
117
|
+
*/
|
|
118
|
+
export function renewSignal(): number | undefined {
|
|
119
|
+
void refreshRenewSignal()
|
|
120
|
+
return signalValue
|
|
121
|
+
}
|