@newmo/graphql-codegen-fake-server-client 0.22.0 → 0.23.0

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.
@@ -2,11 +2,15 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const config_1 = require("./config");
4
4
  const convertName_1 = require("./convertName");
5
+ const method_generators_1 = require("./templates/method-generators");
6
+ const runtime_1 = require("./templates/runtime");
5
7
  const plugin = {
6
8
  plugin(_schema, documents, rawConfig, _info) {
7
9
  const config = (0, config_1.normalizeConfig)(rawConfig);
8
- const _fakeEndpoint = config.fakeServerEndpoint;
9
- const registerOperationResponseType = "{ ok: true } | { ok: false; errors: string[] }"; // Conditional fake types with generic Variables
10
+ const registerOperationResponseType = "{ ok: true }";
11
+ // Get runtime code from template function
12
+ const runtimeCode = (0, runtime_1.getRuntimeCode)();
13
+ // Conditional fake types with generic Variables
10
14
  const conditionRuleTypes = `
11
15
  export type FakeClientAlwaysConditionRule = { type: "always" };
12
16
  export type FakeClientVariablesConditionRule<TVariables = Record<string, any>> = { type: "variables"; value: TVariables };
@@ -15,227 +19,99 @@ export type FakeClientRegisterSequenceOptions<TVariables = Record<string, any>>
15
19
  const indentEachLine = (indent, text) => {
16
20
  return text
17
21
  .split("\n")
18
- .map((line) => `${indent}${line}`)
22
+ .map((line) => indent + line)
19
23
  .join("\n");
20
24
  };
21
25
  const generateFakeClient = (exportsFunctions) => {
22
26
  const indent = " ";
23
- return `\
24
- export type CreateFakeClientOptions = {
25
- /**
26
- * The URL of the fake server
27
- * @example 'http://localhost:4000/fake'
28
- */
29
- fakeServerEndpoint: string;
30
- };
31
- export function createFakeClient(options: CreateFakeClientOptions) {
32
- if(!options.fakeServerEndpoint.endsWith('/fake')) {
33
- throw new Error('fakeServerEndpoint must end with "/fake"');
34
- }
35
- return {
36
- ${exportsFunctions
37
- .flatMap((fn) => {
38
- if (fn.type === "query") {
39
- return [
40
- indentEachLine(`${indent}${indent}`, generateRegisterOperationMethod(fn.name, "options.fakeServerEndpoint")),
41
- indentEachLine(`${indent}${indent}`, generateRegisterOperationErrorMethod(fn.name, "options.fakeServerEndpoint")),
42
- indentEachLine(`${indent}${indent}`, generateCalledQuery(fn.name, `options.fakeServerEndpoint + "/called"`)),
43
- ];
44
- }
45
- if (fn.type === "mutation") {
46
- return [
47
- indentEachLine(`${indent}${indent}`, generateRegisterMutationMethod(fn.name, "options.fakeServerEndpoint")),
48
- indentEachLine(`${indent}${indent}`, generateRegisterMutationErrorMethod(fn.name, "options.fakeServerEndpoint")),
49
- indentEachLine(`${indent}${indent}`, generateCalledMutation(fn.name, `options.fakeServerEndpoint + "/called"`)),
50
- ];
51
- }
52
- throw new Error(`Unknown type${fn}`);
53
- })
54
- .join(",\n")}
55
- };
56
- }`;
27
+ // Use runtime code from template
28
+ return (runtimeCode +
29
+ "\n\n" +
30
+ "export function createFakeClient(options: CreateFakeClientOptions) {\n" +
31
+ " if(!options.fakeServerEndpoint.endsWith('/fake')) {\n" +
32
+ " throw new Error('fakeServerEndpoint must end with \"/fake\"');\n" +
33
+ " }\n" +
34
+ " \n" +
35
+ " // Create request queue for rate limiting\n" +
36
+ " const requestQueue = new RequestQueue();\n" +
37
+ " \n" +
38
+ " return {\n" +
39
+ exportsFunctions
40
+ .flatMap((fn) => {
41
+ if (fn.type === "query") {
42
+ return [
43
+ indentEachLine(indent + indent, generateRegisterOperationMethod(fn.name, "options.fakeServerEndpoint")),
44
+ indentEachLine(indent + indent, generateRegisterOperationErrorMethod(fn.name, "options.fakeServerEndpoint")),
45
+ indentEachLine(indent + indent, generateCalledQueryMethod(fn.name, 'options.fakeServerEndpoint + "/called"')),
46
+ ];
47
+ }
48
+ if (fn.type === "mutation") {
49
+ return [
50
+ indentEachLine(indent + indent, generateRegisterMutationMethod(fn.name, "options.fakeServerEndpoint")),
51
+ indentEachLine(indent + indent, generateRegisterMutationErrorMethod(fn.name, "options.fakeServerEndpoint")),
52
+ indentEachLine(indent + indent, generateCalledMutationMethod(fn.name, 'options.fakeServerEndpoint + "/called"')),
53
+ ];
54
+ }
55
+ throw new Error(`Unknown type${fn}`);
56
+ })
57
+ .join(",\n") +
58
+ "\n };\n}");
57
59
  };
58
60
  const generateRegisterOperationMethod = (name, fakeEndpointVariableName) => {
59
61
  const variablesType = `${(0, convertName_1.convertName)(name, config)}QueryVariables`;
60
- return `async register${name}QueryResponse(sequenceId:string, queryResponse: ${name}Query, sequenceOptions?: FakeClientRegisterSequenceOptions<${variablesType}>): Promise<${registerOperationResponseType}> {
61
- const requestCondition = sequenceOptions?.requestCondition ?? { type: "always" };
62
- return await fetch(${fakeEndpointVariableName}, {
63
- method: 'POST',
64
- headers: {
65
- 'Content-Type': 'application/json',
66
- 'sequence-id': sequenceId
67
- },
68
- body: JSON.stringify({
69
- type: "operation",
70
- operationName: "${name}",
71
- data: queryResponse,
72
- requestCondition: requestCondition
73
- }),
74
- }).then((res) => res.json()) as ${registerOperationResponseType};
75
- }`;
62
+ return (0, method_generators_1.generateRegisterQuery)({
63
+ name,
64
+ variablesType,
65
+ responseType: registerOperationResponseType,
66
+ endpoint: fakeEndpointVariableName,
67
+ });
76
68
  };
77
69
  const generateRegisterOperationErrorMethod = (name, fakeEndpointVariableName) => {
78
- return `async register${name}QueryErrorResponse(sequenceId:string, { errors, responseStatusCode }: { errors: Record<string, unknown>[]; responseStatusCode: number }): Promise<${registerOperationResponseType}> {
79
- return await fetch(${fakeEndpointVariableName}, {
80
- method: 'POST',
81
- headers: {
82
- 'Content-Type': 'application/json',
83
- 'sequence-id': sequenceId
84
- },
85
- body: JSON.stringify({
86
- type: "network-error",
87
- operationName: "${name}",
88
- responseStatusCode,
89
- errors
90
- }),
91
- }).then((res) => res.json()) as ${registerOperationResponseType};
92
- }`;
70
+ return (0, method_generators_1.generateRegisterQueryError)({
71
+ name,
72
+ responseType: registerOperationResponseType,
73
+ endpoint: fakeEndpointVariableName,
74
+ });
93
75
  };
94
76
  const generateRegisterMutationMethod = (name, fakeEndpointVariableName) => {
95
77
  const variablesType = `${(0, convertName_1.convertName)(name, config)}MutationVariables`;
96
- return `async register${name}MutationResponse(sequenceId:string, mutationResponse: ${name}Mutation, sequenceOptions?: FakeClientRegisterSequenceOptions<${variablesType}>): Promise<${registerOperationResponseType}> {
97
- const requestCondition = sequenceOptions?.requestCondition ?? { type: "always" };
98
- return await fetch(${fakeEndpointVariableName}, {
99
- method: 'POST',
100
- headers: {
101
- 'Content-Type': 'application/json',
102
- 'sequence-id': sequenceId
103
- },
104
- body: JSON.stringify({
105
- type: "operation",
106
- operationName: "${name}",
107
- data: mutationResponse,
108
- requestCondition: requestCondition
109
- }),
110
- }).then((res) => res.json()) as ${registerOperationResponseType};
111
- }`;
78
+ return (0, method_generators_1.generateRegisterMutation)({
79
+ name,
80
+ variablesType,
81
+ responseType: registerOperationResponseType,
82
+ endpoint: fakeEndpointVariableName,
83
+ });
112
84
  };
113
85
  const generateRegisterMutationErrorMethod = (name, fakeEndpointVariableName) => {
114
- return `async register${name}MutationErrorResponse(sequenceId:string, { errors, responseStatusCode }: { errors: Record<string, unknown>[]; responseStatusCode: number }): Promise<${registerOperationResponseType}> {
115
- return await fetch(${fakeEndpointVariableName}, {
116
- method: 'POST',
117
- headers: {
118
- 'Content-Type': 'application/json',
119
- 'sequence-id': sequenceId
120
- },
121
- body: JSON.stringify({
122
- type: "network-error",
123
- operationName: "${name}",
124
- responseStatusCode,
125
- errors
126
- }),
127
- }).then((res) => res.json()) as ${registerOperationResponseType};
128
- }`;
86
+ return (0, method_generators_1.generateRegisterMutationError)({
87
+ name,
88
+ responseType: registerOperationResponseType,
89
+ endpoint: fakeEndpointVariableName,
90
+ });
129
91
  };
130
- const generateCalledQuery = (name, calledEndpoint) => {
131
- return `async called${name}Query(sequenceId:string): Promise<{
132
- ok: true;
133
- data: {
134
- requestTimestamp: number;
135
- request: {
136
- headers: Record<string, unknown>;
137
- body: {
138
- operationName: string;
139
- query: string;
140
- variables: ${(0, convertName_1.convertName)(name, config)}QueryVariables;
141
- };
142
- };
143
- response: {
144
- statusCode: number;
145
- headers: Record<string, unknown>;
146
- body: ${(0, convertName_1.convertName)(name, config)}Query;
147
- };
148
- }[]
149
- }> {
150
- return await fetch(${calledEndpoint}, {
151
- method: 'POST',
152
- headers: {
153
- 'Content-Type': 'application/json',
154
- 'sequence-id': sequenceId
155
- },
156
- body: JSON.stringify({
157
- operationName: "${name}"
158
- }),
159
- }).then((res) => res.json()) as {
160
- ok: true;
161
- data: {
162
- requestTimestamp: number;
163
- request: {
164
- headers: Record<string, unknown>;
165
- body: {
166
- operationName: string;
167
- query: string;
168
- variables: ${(0, convertName_1.convertName)(name, config)}QueryVariables;
169
- };
170
- };
171
- response: {
172
- statusCode: number;
173
- headers: Record<string, unknown>;
174
- body: ${(0, convertName_1.convertName)(name, config)}Query;
175
- };
176
- }[];
177
- };
178
- }`;
92
+ const generateCalledQueryMethod = (name, calledEndpoint) => {
93
+ const variablesType = `${(0, convertName_1.convertName)(name, config)}QueryVariables`;
94
+ return (0, method_generators_1.generateCalledQuery)({
95
+ name: (0, convertName_1.convertName)(name, config),
96
+ variablesType,
97
+ endpoint: calledEndpoint,
98
+ });
179
99
  };
180
- const generateCalledMutation = (name, calledEndpoint) => {
181
- return `async called${name}Mutation(sequenceId:string): Promise<{
182
- ok: true;
183
- data: {
184
- requestTimestamp: number;
185
- request: {
186
- headers: Record<string, unknown>;
187
- body: {
188
- operationName: string;
189
- query: string;
190
- variables: ${(0, convertName_1.convertName)(name, config)}MutationVariables;
191
- };
192
- };
193
- response: {
194
- statusCode: number;
195
- headers: Record<string, unknown>;
196
- body: ${(0, convertName_1.convertName)(name, config)}Mutation;
197
- };
198
- }[];
199
- }> {
200
- return await fetch(${calledEndpoint}, {
201
- method: 'POST',
202
- headers: {
203
- 'Content-Type': 'application/json',
204
- 'sequence-id': sequenceId
205
- },
206
- body: JSON.stringify({
207
- operationName: "${name}"
208
- }),
209
- }).then((res) => res.json()) as {
210
- ok: true;
211
- data: {
212
- requestTimestamp: number;
213
- request: {
214
- headers: Record<string, unknown>;
215
- body: {
216
- operationName: string;
217
- query: string;
218
- variables: ${(0, convertName_1.convertName)(name, config)}MutationVariables;
219
- };
220
- };
221
- response: {
222
- statusCode: number;
223
- headers: Record<string, unknown>;
224
- body: ${(0, convertName_1.convertName)(name, config)}Mutation;
225
- };
226
- }[];
227
- }
228
- }`;
100
+ const generateCalledMutationMethod = (name, calledEndpoint) => {
101
+ const variablesType = `${(0, convertName_1.convertName)(name, config)}MutationVariables`;
102
+ return (0, method_generators_1.generateCalledMutation)({
103
+ name: (0, convertName_1.convertName)(name, config),
104
+ variablesType,
105
+ endpoint: calledEndpoint,
106
+ });
229
107
  };
230
108
  const importQueryIdentifierName = (documentName) => {
231
- return `import type { ${(0, convertName_1.convertName)(documentName, config)}Query, ${(0, convertName_1.convertName)(documentName, config)}QueryVariables } from '${config.typesFile}';`;
109
+ return `import type { ${(0, convertName_1.convertName)(documentName, config)}Query, ${(0, convertName_1.convertName)(documentName, config)}QueryVariables } from \'${config.typesFile}\';`;
232
110
  };
233
111
  const importMutationIdentifierName = (documentName) => {
234
- return `import type { ${(0, convertName_1.convertName)(documentName, config)}Mutation, ${(0, convertName_1.convertName)(documentName, config)}MutationVariables } from '${config.typesFile}';`;
112
+ return `import type { ${(0, convertName_1.convertName)(documentName, config)}Mutation, ${(0, convertName_1.convertName)(documentName, config)}MutationVariables } from \'${config.typesFile}\';`;
235
113
  };
236
- return `/* eslint-disable */
237
- // This file was generated by a @newmo/graphql-codegen-fake-server-operation
238
- ${documents
114
+ const importsSection = documents
239
115
  .flatMap((document) => {
240
116
  return document.document?.definitions?.map((definition) => {
241
117
  // query
@@ -252,9 +128,8 @@ ${documents
252
128
  return [];
253
129
  });
254
130
  })
255
- .join("\n")}
256
- ${conditionRuleTypes}
257
- ${generateFakeClient(documents.flatMap((document) => {
131
+ .join("\n");
132
+ const functionsSection = generateFakeClient(documents.flatMap((document) => {
258
133
  const flatMap = document.document?.definitions?.flatMap((definition) => {
259
134
  if (definition.kind === "OperationDefinition" &&
260
135
  definition.operation === "query" &&
@@ -279,8 +154,15 @@ ${generateFakeClient(documents.flatMap((document) => {
279
154
  return [];
280
155
  }) ?? [];
281
156
  return flatMap;
282
- }))}
283
- `;
157
+ }));
158
+ return ("/* eslint-disable */\n" +
159
+ "// This file was generated by a @newmo/graphql-codegen-fake-server-operation\n" +
160
+ importsSection +
161
+ "\n" +
162
+ conditionRuleTypes +
163
+ "\n" +
164
+ functionsSection +
165
+ "\n");
284
166
  },
285
167
  };
286
168
  // GraphQL Codegen Plugin requires CommonJS export
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ // Runtime utilities for generated fake client
3
+ // This file is embedded into the generated code
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.RequestQueue = void 0;
6
+ exports.fetchWithRetry = fetchWithRetry;
7
+ // Request queue implementation for rate limiting
8
+ class RequestQueue {
9
+ queue = [];
10
+ running = 0;
11
+ maxConcurrent = 5; // Reduced default for better stability
12
+ requestDelay = 10; // Small delay to prevent overwhelming the server
13
+ lastRequestTime = 0;
14
+ async add(fn) {
15
+ return new Promise((resolve, reject) => {
16
+ this.queue.push(async () => {
17
+ try {
18
+ // Apply request delay if configured
19
+ if (this.requestDelay > 0) {
20
+ const now = Date.now();
21
+ const timeSinceLastRequest = now - this.lastRequestTime;
22
+ if (timeSinceLastRequest < this.requestDelay) {
23
+ await new Promise(r => setTimeout(r, this.requestDelay - timeSinceLastRequest));
24
+ }
25
+ this.lastRequestTime = Date.now();
26
+ }
27
+ const result = await fn();
28
+ resolve(result);
29
+ }
30
+ catch (error) {
31
+ reject(error);
32
+ }
33
+ });
34
+ this.process();
35
+ });
36
+ }
37
+ async process() {
38
+ if (this.running >= this.maxConcurrent || this.queue.length === 0) {
39
+ return;
40
+ }
41
+ this.running++;
42
+ const fn = this.queue.shift();
43
+ if (fn) {
44
+ await fn();
45
+ this.running--;
46
+ this.process();
47
+ }
48
+ }
49
+ }
50
+ exports.RequestQueue = RequestQueue;
51
+ // Retry helper function with exponential backoff
52
+ async function fetchWithRetry(url, options) {
53
+ const maxAttempts = 3;
54
+ const initialDelay = 100;
55
+ const maxDelay = 2000;
56
+ const backoffFactor = 2;
57
+ // Apply HTTP options with sensible defaults
58
+ const fetchOptions = {
59
+ ...options,
60
+ // Enable keepalive for connection reuse
61
+ keepalive: true,
62
+ // Set a reasonable timeout (30 seconds)
63
+ signal: AbortSignal.timeout(30000),
64
+ };
65
+ let lastError;
66
+ let lastResponse;
67
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
68
+ try {
69
+ const response = await fetch(url, fetchOptions);
70
+ lastResponse = response;
71
+ // Success (2xx) or client error (4xx) - don't retry
72
+ if (response.status < 500) {
73
+ return response;
74
+ }
75
+ // Server error (5xx) - should retry
76
+ if (attempt < maxAttempts - 1) {
77
+ const requestInfo = {
78
+ url,
79
+ status: response.status,
80
+ statusText: response.statusText,
81
+ attempt: attempt + 1,
82
+ maxAttempts,
83
+ operationName: JSON.parse(options.body)?.operationName,
84
+ sequenceId: options.headers?.['sequence-id'],
85
+ };
86
+ console.error(`[FakeClient] Server error, will retry:`, requestInfo);
87
+ // Calculate delay with exponential backoff and jitter
88
+ const baseDelay = Math.min(initialDelay * Math.pow(backoffFactor, attempt), maxDelay);
89
+ const jitter = Math.random() * 0.1 * baseDelay; // 10% jitter
90
+ const delay = baseDelay + jitter;
91
+ console.log(`[FakeClient] Retrying in ${Math.round(delay)}ms...`);
92
+ await new Promise(resolve => setTimeout(resolve, delay));
93
+ continue;
94
+ }
95
+ // Last attempt and still server error
96
+ return response;
97
+ }
98
+ catch (error) {
99
+ lastError = error;
100
+ // Determine if error is retryable
101
+ let shouldRetry = false;
102
+ let errorType = 'unknown';
103
+ if (error instanceof TypeError) {
104
+ // Network errors from fetch (connection failures)
105
+ shouldRetry = true;
106
+ errorType = 'network';
107
+ }
108
+ else if (error instanceof Error && error.name === 'AbortError') {
109
+ // Timeout errors
110
+ shouldRetry = true;
111
+ errorType = 'timeout';
112
+ }
113
+ const requestInfo = {
114
+ url,
115
+ errorType,
116
+ error: error instanceof Error ? error.message : String(error),
117
+ attempt: attempt + 1,
118
+ maxAttempts,
119
+ operationName: JSON.parse(options.body)?.operationName,
120
+ sequenceId: options.headers?.['sequence-id'],
121
+ };
122
+ console.error(`[FakeClient] Request failed:`, requestInfo);
123
+ if (shouldRetry && attempt < maxAttempts - 1) {
124
+ // Calculate delay with exponential backoff and jitter
125
+ const baseDelay = Math.min(initialDelay * Math.pow(backoffFactor, attempt), maxDelay);
126
+ const jitter = Math.random() * 0.1 * baseDelay; // 10% jitter
127
+ const delay = baseDelay + jitter;
128
+ console.log(`[FakeClient] Retrying in ${Math.round(delay)}ms...`);
129
+ await new Promise(resolve => setTimeout(resolve, delay));
130
+ continue;
131
+ }
132
+ // Not retryable or max attempts reached
133
+ throw error;
134
+ }
135
+ }
136
+ // Should not reach here, but just in case
137
+ if (lastResponse) {
138
+ return lastResponse;
139
+ }
140
+ throw new Error('Max retry attempts reached', { cause: lastError });
141
+ }
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getRuntimeCode = getRuntimeCode;
4
+ /**
5
+ * Returns the runtime code as a string template
6
+ * This is used to embed the runtime code in the generated client
7
+ */
8
+ function getRuntimeCode() {
9
+ return `// Runtime utilities for generated fake client
10
+ export type CreateFakeClientOptions = {
11
+ /**
12
+ * The URL of the fake server
13
+ * @example 'http://localhost:4000/fake'
14
+ */
15
+ fakeServerEndpoint: string;
16
+ };
17
+
18
+ // Request queue implementation for rate limiting
19
+ class RequestQueue {
20
+ private queue: Array<() => Promise<any>> = [];
21
+ private running = 0;
22
+ private maxConcurrent: number = 5; // Reduced default for better stability
23
+ private requestDelay: number = 10; // Small delay to prevent overwhelming the server
24
+ private lastRequestTime = 0;
25
+
26
+ async add<T>(fn: () => Promise<T>): Promise<T> {
27
+ return new Promise((resolve, reject) => {
28
+ this.queue.push(async () => {
29
+ try {
30
+ // Apply request delay if configured
31
+ if (this.requestDelay > 0) {
32
+ const now = Date.now();
33
+ const timeSinceLastRequest = now - this.lastRequestTime;
34
+ if (timeSinceLastRequest < this.requestDelay) {
35
+ await new Promise(r => setTimeout(r, this.requestDelay - timeSinceLastRequest));
36
+ }
37
+ this.lastRequestTime = Date.now();
38
+ }
39
+
40
+ const result = await fn();
41
+ resolve(result);
42
+ } catch (error) {
43
+ reject(error);
44
+ }
45
+ });
46
+ this.process();
47
+ });
48
+ }
49
+
50
+ private async process() {
51
+ if (this.running >= this.maxConcurrent || this.queue.length === 0) {
52
+ return;
53
+ }
54
+
55
+ this.running++;
56
+ const fn = this.queue.shift();
57
+ if (fn) {
58
+ await fn();
59
+ this.running--;
60
+ this.process();
61
+ }
62
+ }
63
+ }
64
+
65
+ // Retry helper function with exponential backoff
66
+ async function fetchWithRetry(
67
+ url: string,
68
+ options: RequestInit
69
+ ): Promise<Response> {
70
+ const maxAttempts = 3;
71
+ const initialDelay = 100;
72
+ const maxDelay = 2000;
73
+ const backoffFactor = 2;
74
+
75
+ // Apply HTTP options with sensible defaults
76
+ const fetchOptions: RequestInit = {
77
+ ...options,
78
+ // Enable keepalive for connection reuse
79
+ keepalive: true,
80
+ // Set a reasonable timeout (30 seconds)
81
+ signal: AbortSignal.timeout(30000),
82
+ };
83
+
84
+ let lastError: Error | undefined;
85
+ let lastResponse: Response | undefined;
86
+
87
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
88
+ try {
89
+ const response = await fetch(url, fetchOptions);
90
+ lastResponse = response;
91
+
92
+ // Success (2xx) or client error (4xx) - don't retry
93
+ if (response.status < 500) {
94
+ return response;
95
+ }
96
+
97
+ // Server error (5xx) - should retry
98
+ if (attempt < maxAttempts - 1) {
99
+ const requestInfo = {
100
+ url,
101
+ status: response.status,
102
+ statusText: response.statusText,
103
+ attempt: attempt + 1,
104
+ maxAttempts,
105
+ operationName: JSON.parse(options.body as string)?.operationName,
106
+ sequenceId: (options.headers as any)?.['sequence-id'],
107
+ };
108
+
109
+ console.error(\`[FakeClient] Server error, will retry:\`, requestInfo);
110
+
111
+ // Calculate delay with exponential backoff and jitter
112
+ const baseDelay = Math.min(initialDelay * Math.pow(backoffFactor, attempt), maxDelay);
113
+ const jitter = Math.random() * 0.1 * baseDelay; // 10% jitter
114
+ const delay = baseDelay + jitter;
115
+
116
+ console.log(\`[FakeClient] Retrying in \${Math.round(delay)}ms...\`);
117
+ await new Promise(resolve => setTimeout(resolve, delay));
118
+ continue;
119
+ }
120
+
121
+ // Last attempt and still server error
122
+ return response;
123
+
124
+ } catch (error) {
125
+ lastError = error as Error;
126
+
127
+ // Determine if error is retryable
128
+ let shouldRetry = false;
129
+ let errorType = 'unknown';
130
+
131
+ if (error instanceof TypeError) {
132
+ // Network errors from fetch (connection failures)
133
+ shouldRetry = true;
134
+ errorType = 'network';
135
+ } else if (error instanceof Error && error.name === 'AbortError') {
136
+ // Timeout errors
137
+ shouldRetry = true;
138
+ errorType = 'timeout';
139
+ }
140
+
141
+ const requestInfo = {
142
+ url,
143
+ errorType,
144
+ error: error instanceof Error ? error.message : String(error),
145
+ attempt: attempt + 1,
146
+ maxAttempts,
147
+ operationName: JSON.parse(options.body as string)?.operationName,
148
+ sequenceId: (options.headers as any)?.['sequence-id'],
149
+ };
150
+
151
+ console.error(\`[FakeClient] Request failed:\`, requestInfo);
152
+
153
+ if (shouldRetry && attempt < maxAttempts - 1) {
154
+ // Calculate delay with exponential backoff and jitter
155
+ const baseDelay = Math.min(initialDelay * Math.pow(backoffFactor, attempt), maxDelay);
156
+ const jitter = Math.random() * 0.1 * baseDelay; // 10% jitter
157
+ const delay = baseDelay + jitter;
158
+
159
+ console.log(\`[FakeClient] Retrying in \${Math.round(delay)}ms...\`);
160
+ await new Promise(resolve => setTimeout(resolve, delay));
161
+ continue;
162
+ }
163
+
164
+ // Not retryable or max attempts reached
165
+ throw error;
166
+ }
167
+ }
168
+
169
+ // Should not reach here, but just in case
170
+ if (lastResponse) {
171
+ return lastResponse;
172
+ }
173
+ throw new Error('Max retry attempts reached', { cause: lastError });
174
+ }`;
175
+ }