@octanejs/apollo-client 0.1.8 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -77,6 +77,32 @@ For tests, import Apollo's framework-neutral mocks from
|
|
|
77
77
|
`@octanejs/apollo-client/testing` and the Octane `MockedProvider` from
|
|
78
78
|
`@octanejs/apollo-client/testing/react`.
|
|
79
79
|
|
|
80
|
+
## Server rendering
|
|
81
|
+
|
|
82
|
+
Apollo's `useQuery` does not suspend, so a single server-rendering pass cannot
|
|
83
|
+
wait for network data. Use the Apollo-aware prepass to retain subscriptions,
|
|
84
|
+
finish nested queries, and return Octane's final HTML and scoped CSS:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { createElement } from 'octane';
|
|
88
|
+
import { renderToString } from 'octane/server';
|
|
89
|
+
import { prerenderStatic } from '@octanejs/apollo-client/react/ssr';
|
|
90
|
+
|
|
91
|
+
const rendered = await prerenderStatic({
|
|
92
|
+
tree: createElement(App, { client }),
|
|
93
|
+
renderFunction: renderToString,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const { html, css } = rendered.renderFnResult;
|
|
97
|
+
const apolloState = client.extract();
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Create a separate Apollo client for every server request. Restore `apolloState`
|
|
101
|
+
into the browser client's `InMemoryCache` before calling `hydrateRoot` so cached
|
|
102
|
+
queries adopt the server markup without duplicate network requests. Queries
|
|
103
|
+
using `ssr: false` or `fetchPolicy: 'no-cache'` remain client-only, matching
|
|
104
|
+
Apollo 4.2.6. Streaming cache patches are not provided.
|
|
105
|
+
|
|
80
106
|
## Compatibility
|
|
81
107
|
|
|
82
108
|
- Hooks and public TypeScript overloads track Apollo Client 4.2.6.
|
|
@@ -88,8 +114,8 @@ For tests, import Apollo's framework-neutral mocks from
|
|
|
88
114
|
- React Server Components and Apollo's React Compiler output are out of scope.
|
|
89
115
|
- `MockedProvider` accepts normal Octane children blocks; use a descriptor-style
|
|
90
116
|
`children={<App />}` prop when `childProps` must be cloned onto the child.
|
|
91
|
-
- Apollo-aware buffered SSR
|
|
92
|
-
|
|
117
|
+
- Apollo-aware buffered SSR is available from `/react/ssr`; streaming cache
|
|
118
|
+
patches remain out of scope.
|
|
93
119
|
|
|
94
120
|
The adapter sources are derived from Apollo Client at commit
|
|
95
121
|
`f934b60720fc828a61e04b00988eeefb83d273bc`, under Apollo's MIT license. Local
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/apollo-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
"types": "./src/react/internal/index.d.ts",
|
|
41
41
|
"default": "./src/react/internal/index.js"
|
|
42
42
|
},
|
|
43
|
+
"./react/ssr": {
|
|
44
|
+
"types": "./src/react/ssr/index.d.ts",
|
|
45
|
+
"default": "./src/react/ssr/index.js"
|
|
46
|
+
},
|
|
43
47
|
"./testing": {
|
|
44
48
|
"types": "./src/testing/index.d.ts",
|
|
45
49
|
"default": "./src/testing/index.js"
|
|
@@ -58,18 +62,18 @@
|
|
|
58
62
|
"@apollo/client": ">=4.2.6 <4.3.0",
|
|
59
63
|
"graphql": "^16.0.0 || ^17.0.0",
|
|
60
64
|
"rxjs": "^7.3.0",
|
|
61
|
-
"octane": "0.1.
|
|
65
|
+
"octane": "0.1.17"
|
|
62
66
|
},
|
|
63
67
|
"devDependencies": {
|
|
64
68
|
"@apollo/client": "4.2.6",
|
|
65
|
-
"@tsrx/react": "^0.2.
|
|
69
|
+
"@tsrx/react": "^0.2.50",
|
|
66
70
|
"esbuild": "^0.28.1",
|
|
67
71
|
"graphql": "^16.14.2",
|
|
68
72
|
"react": "^19.2.7",
|
|
69
73
|
"react-dom": "^19.2.7",
|
|
70
74
|
"rxjs": "^7.8.2",
|
|
71
75
|
"vitest": "^4.1.10",
|
|
72
|
-
"octane": "0.1.
|
|
76
|
+
"octane": "0.1.17"
|
|
73
77
|
},
|
|
74
78
|
"scripts": {
|
|
75
79
|
"test": "vitest run --root ../.. --project apollo-client"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { prerenderStatic } from './prerenderStatic.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApolloClient,
|
|
3
|
+
DocumentNode,
|
|
4
|
+
ObservableQuery,
|
|
5
|
+
OperationVariables,
|
|
6
|
+
} from '@apollo/client';
|
|
7
|
+
import type { OctaneNode } from 'octane';
|
|
8
|
+
import type { RenderResult } from 'octane/server';
|
|
9
|
+
|
|
10
|
+
export interface PrerenderStaticInternalContext {
|
|
11
|
+
getObservableQuery(
|
|
12
|
+
query: DocumentNode,
|
|
13
|
+
variables?: Record<string, unknown>,
|
|
14
|
+
): ObservableQuery | undefined;
|
|
15
|
+
onCreatedObservableQuery(
|
|
16
|
+
observable: ObservableQuery,
|
|
17
|
+
query: DocumentNode,
|
|
18
|
+
variables: OperationVariables,
|
|
19
|
+
): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare namespace prerenderStatic {
|
|
23
|
+
type ServerTree = OctaneNode | (() => OctaneNode);
|
|
24
|
+
type PrerenderFunction = (
|
|
25
|
+
component: () => OctaneNode,
|
|
26
|
+
) => string | RenderResult | PromiseLike<string | RenderResult>;
|
|
27
|
+
|
|
28
|
+
interface Options<Prerender extends PrerenderFunction = PrerenderFunction> {
|
|
29
|
+
tree: ServerTree;
|
|
30
|
+
context?: { client?: ApolloClient };
|
|
31
|
+
renderFunction: Prerender;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
ignoreResults?: boolean;
|
|
34
|
+
diagnostics?: boolean;
|
|
35
|
+
maxRerenders?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Diagnostics {
|
|
39
|
+
renderCount: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface Result<Prerender extends PrerenderFunction = PrerenderFunction> {
|
|
43
|
+
result: string;
|
|
44
|
+
renderFnResult: Awaited<ReturnType<Prerender>>;
|
|
45
|
+
aborted: boolean;
|
|
46
|
+
diagnostics?: Diagnostics;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export declare function prerenderStatic<
|
|
51
|
+
Prerender extends prerenderStatic.PrerenderFunction = prerenderStatic.PrerenderFunction,
|
|
52
|
+
>(options: prerenderStatic.Options<Prerender>): Promise<prerenderStatic.Result<Prerender>>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { print } from 'graphql';
|
|
2
|
+
import { createElement } from 'octane';
|
|
3
|
+
import { filter, firstValueFrom } from 'rxjs';
|
|
4
|
+
import { canonicalStringify } from '@apollo/client/utilities';
|
|
5
|
+
import { invariant } from '@apollo/client/utilities/invariant';
|
|
6
|
+
|
|
7
|
+
import { getApolloContext } from '../context/ApolloContext.js';
|
|
8
|
+
import { wrapperSymbol } from '../internal/index.js';
|
|
9
|
+
import { useSSRQuery } from './useSSRQuery.js';
|
|
10
|
+
|
|
11
|
+
function getObservableQueryKey(query, variables = {}) {
|
|
12
|
+
return `${print(query)}|${canonicalStringify(variables)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const noopObserver = { complete() {} };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Render an Octane tree repeatedly until Apollo's non-suspending useQuery
|
|
19
|
+
* operations settle. The final Octane render result retains its scoped CSS.
|
|
20
|
+
*/
|
|
21
|
+
export function prerenderStatic({
|
|
22
|
+
tree,
|
|
23
|
+
context = {},
|
|
24
|
+
renderFunction,
|
|
25
|
+
signal,
|
|
26
|
+
ignoreResults,
|
|
27
|
+
diagnostics,
|
|
28
|
+
maxRerenders = 50,
|
|
29
|
+
}) {
|
|
30
|
+
const availableObservableQueries = new Map();
|
|
31
|
+
const subscriptions = new Set();
|
|
32
|
+
const recentlyCreatedObservableQueries = new Set();
|
|
33
|
+
let renderCount = 0;
|
|
34
|
+
|
|
35
|
+
const internalContext = {
|
|
36
|
+
getObservableQuery(query, variables) {
|
|
37
|
+
return availableObservableQueries.get(getObservableQueryKey(query, variables));
|
|
38
|
+
},
|
|
39
|
+
onCreatedObservableQuery(observable, query, variables) {
|
|
40
|
+
availableObservableQueries.set(getObservableQueryKey(query, variables), observable);
|
|
41
|
+
subscriptions.add(observable.subscribe(noopObserver));
|
|
42
|
+
if (observable.options.fetchPolicy !== 'cache-only') {
|
|
43
|
+
recentlyCreatedObservableQueries.add(observable);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
async function process() {
|
|
49
|
+
renderCount += 1;
|
|
50
|
+
invariant(renderCount <= maxRerenders, 25, maxRerenders);
|
|
51
|
+
invariant(!signal?.aborted, 26);
|
|
52
|
+
|
|
53
|
+
const ApolloContext = getApolloContext();
|
|
54
|
+
const child = typeof tree === 'function' ? createElement(tree, {}) : tree;
|
|
55
|
+
function ApolloSSRTree() {
|
|
56
|
+
return createElement(ApolloContext.Provider, {
|
|
57
|
+
value: {
|
|
58
|
+
...context,
|
|
59
|
+
[wrapperSymbol]: {
|
|
60
|
+
useQuery: () => useSSRQuery.bind(internalContext),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
children: child,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const renderFnResult = await renderFunction(ApolloSSRTree);
|
|
68
|
+
const result = consume(renderFnResult, ignoreResults);
|
|
69
|
+
if (recentlyCreatedObservableQueries.size === 0) {
|
|
70
|
+
return { result, renderFnResult, aborted: signal?.aborted ?? false };
|
|
71
|
+
}
|
|
72
|
+
if (signal?.aborted) {
|
|
73
|
+
return { result, renderFnResult, aborted: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const dataPromise = Promise.all(
|
|
77
|
+
Array.from(recentlyCreatedObservableQueries, async (observable) => {
|
|
78
|
+
await firstValueFrom(observable.pipe(filter((value) => value.loading === false)));
|
|
79
|
+
recentlyCreatedObservableQueries.delete(observable);
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (signal) {
|
|
84
|
+
let resolveAbort;
|
|
85
|
+
const abortPromise = new Promise((resolve) => {
|
|
86
|
+
resolveAbort = resolve;
|
|
87
|
+
});
|
|
88
|
+
signal.addEventListener('abort', resolveAbort);
|
|
89
|
+
try {
|
|
90
|
+
await Promise.race([abortPromise, dataPromise]);
|
|
91
|
+
} finally {
|
|
92
|
+
signal.removeEventListener('abort', resolveAbort);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
await dataPromise;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (signal?.aborted) {
|
|
99
|
+
return { result, renderFnResult, aborted: true };
|
|
100
|
+
}
|
|
101
|
+
return process();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Promise.resolve()
|
|
105
|
+
.then(process)
|
|
106
|
+
.then((result) => (diagnostics ? { ...result, diagnostics: { renderCount } } : result))
|
|
107
|
+
.finally(() => {
|
|
108
|
+
availableObservableQueries.clear();
|
|
109
|
+
recentlyCreatedObservableQueries.clear();
|
|
110
|
+
for (const subscription of subscriptions) subscription.unsubscribe();
|
|
111
|
+
subscriptions.clear();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function consume(value, ignoreResults) {
|
|
116
|
+
if (typeof value === 'string') return ignoreResults ? '' : value;
|
|
117
|
+
if (value && typeof value.html === 'string' && typeof value.css === 'string') {
|
|
118
|
+
return ignoreResults ? '' : value.html;
|
|
119
|
+
}
|
|
120
|
+
throw new Error(
|
|
121
|
+
'`prerenderStatic` requires an Octane renderer returning `{ html, css }` or a string.',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { NetworkStatus } from '@apollo/client';
|
|
2
|
+
import { maybeDeepFreeze } from '@apollo/client/utilities/internal';
|
|
3
|
+
|
|
4
|
+
import { skipToken } from '../hooks/constants.js';
|
|
5
|
+
import { useApolloClient } from '../hooks/useApolloClient.js';
|
|
6
|
+
import { useQuery } from '../hooks/useQuery.js';
|
|
7
|
+
|
|
8
|
+
const skipStandbyResult = maybeDeepFreeze({
|
|
9
|
+
loading: false,
|
|
10
|
+
data: undefined,
|
|
11
|
+
dataState: 'empty',
|
|
12
|
+
error: undefined,
|
|
13
|
+
networkStatus: NetworkStatus.ready,
|
|
14
|
+
partial: true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function withoutObservableAccess(value) {
|
|
18
|
+
Object.defineProperty(value, 'observable', {
|
|
19
|
+
get() {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'"observable" property is not accessible on skipped hooks or hook calls with `ssr: false` during SSR',
|
|
22
|
+
);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Matches Apollo Client 4.2.6's SSR hook while using Octane's provider and
|
|
29
|
+
// call-site-slotted useApolloClient. The prepass owns the observable lifetime.
|
|
30
|
+
export const useSSRQuery = function (query, options = {}) {
|
|
31
|
+
'use no memo';
|
|
32
|
+
|
|
33
|
+
function notAllowed() {
|
|
34
|
+
throw new Error('This method cannot be called during SSR.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const client = useApolloClient(typeof options === 'object' ? options.client : undefined);
|
|
38
|
+
const baseResult = {
|
|
39
|
+
client,
|
|
40
|
+
refetch: notAllowed,
|
|
41
|
+
fetchMore: notAllowed,
|
|
42
|
+
subscribeToMore: notAllowed,
|
|
43
|
+
updateQuery: notAllowed,
|
|
44
|
+
startPolling: notAllowed,
|
|
45
|
+
stopPolling: notAllowed,
|
|
46
|
+
variables: typeof options === 'object' ? options.variables : undefined,
|
|
47
|
+
previousData: undefined,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (options === skipToken || options.skip || options.fetchPolicy === 'standby') {
|
|
51
|
+
return withoutObservableAccess({ ...baseResult, ...skipStandbyResult });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (options.ssr === false || options.fetchPolicy === 'no-cache') {
|
|
55
|
+
return withoutObservableAccess({ ...baseResult, ...useQuery.ssrDisabledResult });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let observable = this.getObservableQuery(query, options.variables);
|
|
59
|
+
if (!observable) {
|
|
60
|
+
observable = client.watchQuery({
|
|
61
|
+
query,
|
|
62
|
+
...options,
|
|
63
|
+
fetchPolicy:
|
|
64
|
+
options.fetchPolicy === 'network-only' || options.fetchPolicy === 'cache-and-network'
|
|
65
|
+
? 'cache-first'
|
|
66
|
+
: options.fetchPolicy,
|
|
67
|
+
});
|
|
68
|
+
this.onCreatedObservableQuery(observable, query, options.variables);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
observable,
|
|
73
|
+
...observable.getCurrentResult(),
|
|
74
|
+
...baseResult,
|
|
75
|
+
};
|
|
76
|
+
};
|