@octanejs/apollo-client 0.1.8 → 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.8",
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"
@@ -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.13"
65
+ "octane": "0.1.16"
62
66
  },
63
67
  "devDependencies": {
64
68
  "@apollo/client": "4.2.6",
65
- "@tsrx/react": "^0.2.46",
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.13"
76
+ "octane": "0.1.16"
73
77
  },
74
78
  "scripts": {
75
79
  "test": "vitest run --root ../.. --project apollo-client"
@@ -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
+ };