@newmo/graphql-codegen-fake-server-client 0.22.0 → 0.23.1
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/dist/graphql-codegen-fake-server-client.js +89 -207
- package/dist/runtime/fake-client-runtime.js +141 -0
- package/dist/runtime-template.js +175 -0
- package/dist/template-helpers.js +291 -0
- package/dist/templates/method-generators.js +294 -0
- package/dist/templates/runtime.js +175 -0
- package/package.json +2 -2
- package/src/graphql-codegen-fake-server-client.ts +192 -276
- package/src/templates/method-generators.ts +324 -0
- package/src/templates/runtime.ts +172 -0
|
@@ -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
|
|
9
|
-
|
|
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) =>
|
|
22
|
+
.map((line) => indent + line)
|
|
19
23
|
.join("\n");
|
|
20
24
|
};
|
|
21
25
|
const generateFakeClient = (exportsFunctions) => {
|
|
22
26
|
const indent = " ";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|