@octanejs/apollo-client 0.1.7 → 0.1.11

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/cache extraction is not included in this first
92
- client release.
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.7",
3
+ "version": "0.1.11",
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"
@@ -50,26 +54,26 @@
50
54
  }
51
55
  },
52
56
  "dependencies": {
53
- "@graphql-typed-document-node/core": "^3.1.1",
54
- "@wry/equality": "^0.5.6",
57
+ "@graphql-typed-document-node/core": "^3.2.0",
58
+ "@wry/equality": "^0.5.7",
55
59
  "@wry/trie": "^0.5.0"
56
60
  },
57
61
  "peerDependencies": {
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.12"
65
+ "octane": "0.1.16"
62
66
  },
63
67
  "devDependencies": {
64
68
  "@apollo/client": "4.2.6",
65
- "@tsrx/react": "^0.2.44",
69
+ "@tsrx/react": "^0.2.50",
66
70
  "esbuild": "^0.28.1",
67
- "graphql": "^16.11.0",
68
- "react": "^19.2.0",
69
- "react-dom": "^19.2.0",
71
+ "graphql": "^16.14.2",
72
+ "react": "^19.2.7",
73
+ "react-dom": "^19.2.7",
70
74
  "rxjs": "^7.8.2",
71
- "vitest": "^4.1.9",
72
- "octane": "0.1.12"
75
+ "vitest": "^4.1.10",
76
+ "octane": "0.1.16"
73
77
  },
74
78
  "scripts": {
75
79
  "test": "vitest run --root ../.. --project apollo-client"
@@ -196,28 +196,21 @@ export declare namespace useBackgroundQuery {
196
196
  queryRef: TOptions extends any
197
197
  ? TOptions extends SkipToken
198
198
  ? undefined
199
- :
200
- | QueryRef<
201
- TData,
202
- TVariables,
203
- | 'complete'
204
- | 'streaming'
205
- | (
206
- | (OptionWithFallback<TOptions, DefaultOptions, 'errorPolicy'> extends 'none'
207
- ? never
208
- : 'empty')
209
- | (OptionWithFallback<
210
- TOptions,
211
- DefaultOptions,
212
- 'returnPartialData'
213
- > extends false
214
- ? never
215
- : 'partial')
216
- )
217
- >
218
- | (OptionWithFallback<TOptions, DefaultOptions, 'skip'> extends false
219
- ? never
220
- : undefined)
199
+ : | QueryRef<
200
+ TData,
201
+ TVariables,
202
+ | 'complete'
203
+ | 'streaming'
204
+ | (
205
+ | (OptionWithFallback<TOptions, DefaultOptions, 'errorPolicy'> extends 'none'
206
+ ? never
207
+ : 'empty')
208
+ | (OptionWithFallback<TOptions, DefaultOptions, 'returnPartialData'> extends false
209
+ ? never
210
+ : 'partial')
211
+ )
212
+ >
213
+ | (OptionWithFallback<TOptions, DefaultOptions, 'skip'> extends false ? never : undefined)
221
214
  : never,
222
215
  result: useBackgroundQuery.Result<TData, TVariables>,
223
216
  ];
@@ -35,9 +35,7 @@ export declare namespace useFragment {
35
35
  * be retrieved, or a `{ __ref: "..." }` reference, or a `string` ID (uncommon).
36
36
  */
37
37
  from:
38
- | useFragment.FromOptionValue<TData>
39
- | Array<useFragment.FromOptionValue<TData> | null>
40
- | null;
38
+ useFragment.FromOptionValue<TData> | Array<useFragment.FromOptionValue<TData> | null> | null;
41
39
  /**
42
40
  * Whether to read from optimistic or non-optimistic cache data. If
43
41
  * this named option is provided, the optimistic parameter of the
@@ -234,16 +234,13 @@ export declare namespace useSuspenseQuery {
234
234
  | (TOptions extends any
235
235
  ? TOptions extends SkipToken
236
236
  ? 'empty'
237
- :
238
- | (OptionWithFallback<TOptions, DefaultOptions, 'errorPolicy'> extends 'none'
239
- ? never
240
- : 'empty')
241
- | (OptionWithFallback<TOptions, DefaultOptions, 'skip'> extends false
242
- ? never
243
- : 'empty')
244
- | (OptionWithFallback<TOptions, DefaultOptions, 'returnPartialData'> extends false
245
- ? never
246
- : 'partial')
237
+ : | (OptionWithFallback<TOptions, DefaultOptions, 'errorPolicy'> extends 'none'
238
+ ? never
239
+ : 'empty')
240
+ | (OptionWithFallback<TOptions, DefaultOptions, 'skip'> extends false ? never : 'empty')
241
+ | (OptionWithFallback<TOptions, DefaultOptions, 'returnPartialData'> extends false
242
+ ? never
243
+ : 'partial')
247
244
  : never)
248
245
  | ([TOptions] extends [SkipToken]
249
246
  ? DefaultOptions extends {
@@ -0,0 +1,2 @@
1
+ export { prerenderStatic } from './prerenderStatic.js';
2
+ export type { PrerenderStaticInternalContext } from './prerenderStatic.js';
@@ -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
+ };